【重拾Handler】——postDelayed原理你真的了解吗?

Handler已经算是一个老生常谈的知识点,但是最近在回顾的源码的时候,发现Handler其实远不止当初看源码想象的那么简单。

前言

本篇博客将对postDelayed的原理进行分析,起初我以为这块并不需要费过多的精力,但是事情往往需要结合实例,如下面一段代码

        handler.postDelayed(new Runnable() {
            @Override
            public void run() {

            }
        }, 500);
        handler.post(new Runnable() {
            @Override
            public void run() {

            }
        });

可以看到这里利用Hanlder连续发送了两个消息,其中第一个是延时了500ms,哪这种情况下的Handler是怎么执行的?这时候我就犯嘀咕了,可能心里明白,但是嘴上不能说清,这时候就说明需要翻看一下源码了。

源码分析

    public final boolean postDelayed(Runnable r, long delayMillis)
    {
        return sendMessageDelayed(getPostMessage(r), delayMillis);
    }
    
        public final boolean sendMessageDelayed(Message msg, long delayMillis)
    {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

这里首先可以看到,我们利用postDelayed时,传入的时间,最后都会和当前的时间做加和的,而不是单纯的只是用延时时间。
熟悉Handler机制的应该都明白,当我们利用Handler发送消息时,最后的实质都会向MessageQueue插入消息了,最终都会执行到enqueueMessage方法。

    boolean enqueueMessage(Message msg, long when) {
        ...
        synchronized (this) {
            ...
            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                //当延时时间小于当前链表头到消息的执行时间
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        //遍历找到合适的插入时间
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                //插入消息链表
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            //唤醒休眠
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

这里可以看到,最核心的判断条件

if (p == null || when == 0 || when < p.when) {
            // New head, wake up the event queue if blocked.
            //当延时时间小于当前链表头到消息的执行时间
        } else {
        //延时比链表头的时间要长
        }

这里拿插入的消息的延时和当前链表头的时间点对比,如果比当前链表头的时间靠前,则新插入的消息变为新的消息头,如果比链表头的要长,则可以看到,这里利用for循环遍历寻找合适的时间点,也就是时间比当前要插入的延时还要长的时间点,将新插入的消息插入到这个位置。
所以这里可以先得出一个结论

延时消息会和当前消息队列里的消息头的执行时间做对比,如果比头的时间靠前,则会做为新的消息头,不然则会从消息头开始向后遍历,找到合适的位置插入延时消息。

后面这个判断也是非常重要

if (needWake) {
                nativeWake(mPtr);
            }

这里是用于判断Handler是否需要唤醒休眠等待消息的执行的,具体判断条件可以看出是和needWake变量有关。可以看到,这里needWake变量是和mBlocked这个变量的值密切相关。这里我们来看一下mBlocked变量的定义。

// Indicates whether next() is blocked waiting in pollOnce() with a non-zero timeout.
    private boolean mBlocked;

通过注释可以先简单的理解一下,这个变量是表明在执行next()方法是否在等待一个有延时的消息而被阻塞。既然提到了next()方法,我们肯定要看一下这个方法。

Message next() {
        // Return here if the message loop has already quit and been disposed.
        // This can happen if the application tries to restart a looper after quit
        // which is not supported.
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }

        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
            //native层休眠
            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        //如果当前消息头是延时消息,比当前时间长的话,计算延时等待的时间并赋值给nextPollTimeoutMillis
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                    //取出消息头,执行
                        // Got a message.
                        //将mBlocked设置为false
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

                // Process the quit message now that all pending messages have been handled.
                if (mQuitting) {
                    dispose();
                    return null;
                }

                // If first time idle, then get the number of idlers to run.
                // Idle handles only run if the queue is empty or if the first message
                // in the queue (possibly a barrier) is due to be handled in the future.
                if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
                    // No idle handlers to run.  Loop and wait some more.
                    //没有IdleHandler,则将mBlocked设置为true
                    mBlocked = true;
                    continue;
                }

                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

            // Reset the idle handler count to 0 so we do not run them again.
            pendingIdleHandlerCount = 0;

            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }

这里可以看到几个我着重注释到地方

    1. nativePollOnce(ptr, nextPollTimeoutMillis);会根据nextPollTimeoutMillis的值确定是否休眠,如果此时nextPollTimeoutMillis的值大于0,则next()方法会在这里休眠等待唤醒。
    1. 从消息头取消息会和当前时间做对比,如果需要延时,则计算延时时间,并赋值给nextPollTimeoutMillis。
    1. 如果不需要延时,则正常取出消息头,并将mBlocked设置为false
    1. 如果idleHandler数量为0,则将mBlocked设置为true。

实际场景

所以到此我们可以结合实际场景分析一下Handler对于postDelayed的原理。

1.消息队列中目前没有消息,然后postDelay一个延时消息。

由于消息队列目前没有消息,所以在执行next()方法时

if (msg != null) {
                  ...
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

会将nextPollTimeoutMillis设置为-1,而且由于没有idleHandler,所以mBlocked=true。
这时,for循环再执行到nativePollOnce(ptr, nextPollTimeoutMillis);则会休眠等待唤醒。
此时postDelayed发送一个消息,再执行enqueueMessage方法时,

if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }
 // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }

由于当前的头消息为null,所以新插入的延时消息直接作为头消息,而mBlocked=true,所以needWake=true
所以变会执行nativeWake(mPtr);方法,底层唤醒休眠,这时next()方法变会从nativePollOnce(ptr, nextPollTimeoutMillis);唤醒,继续取消息执行。
这里可能有人有疑问,这时取消息,便是我们插入的延时消息,那么肯定又休眠等待唤醒了,此时如果没有新的消息插入,谁来唤醒延时消息执行呢?
这里nativePollOnce(ptr, nextPollTimeoutMillis);其实是有自动的唤醒机制的,也就是说除了利用nativeWake(mPtr);被动唤醒,底层自动也会唤醒自身执行延时消息的。

2.消息队列中没有消息,先插入一个延时消息,再插入一个正常的不延时的消息。

这个场景就可以接着上面那个分析,当插入一个延时消息后,会进入休眠等待的过程,这时mBlocked=true,这时再插入一个消息。

if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }

            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }

由于此时消息头是延时消息,新插入的消息不是延时消息,肯定比延时消息的时间小,所以新插入的消息替换延时消息,变成新的消息头,并且needWake=mBlocked=true,所以会执行nativeWake(mPtr);方法,唤醒休眠,这时再执行next()方法中的for循环,便会取消息头,就会取到新插入的不需要延时的消息,并执行,接着由于后面的消息就是延时消息,所以会进行新一轮的休眠等待。

拓展

基于以上的分析其实大家对于Handler肯定又有了一个更新的认识,这里再拓展几个问题:

Handler的休眠具体指的什么?会不会阻塞UI?

这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。

Handler的Delay不一定会在when的时间执行

(1)在Loop.loop()中是顺序处理消息,如果前一个消息处理耗时较长,完成之后已经超过了when,消息不可能在when时间点被处理。
(2)即使when的时间点没有被处理其他消息所占用,线程也有可能被调度失去cpu时间片。
(3)在等待时间点when的过程中有可能入队处理时间更早的消息,会被优先处理,又增加了(1)的可能性。
所以由上述三点可知,Handler提供的指定处理时间的api诸如postDelayed()/postAtTime()/sendMessageDelayed()/sendMessageAtTime(),只能保证在指定时间之前不被执行,不能保证在指定时间点被执行。

相关博客推荐

postDelayed原理
Handler管道的原理

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容

  • 【Android Handler 消息机制】 前言 在Android开发中,我们都知道不能在主线程中执行耗时的任务...
    Rtia阅读 4,809评论 1 28
  • 1. 前言 在之前的图解Handler原理最后留下了几个课后题,如果还没看过那篇文章的,建议先看那篇文章,课后题如...
    唐江旭阅读 5,924评论 5 45
  • 为了更好的理解 Looper 的工作原理,我们需要对 ThreadLocal 进行了解,如果对 ThreadLoc...
    墨染书阅读 1,463评论 0 3
  • 在Android系统中,Handler机制应用较广,尤其是在App层面,基本每个App都会用到。使用的场景主要是向...
    路路人王阅读 751评论 0 49
  • 就说我家外孙吧,已经有13天没有大便了,女儿也没有和我说,她们一直在她婆家住,前两天才回来,和我说了以后我就开始投...
    宋亿青s六中换阅读 154评论 0 0