从MessageQueue视角理解Handler

从MessageQueue视角理解Handler

Android小彩虹2021-08-22 14:32:29270A+A-

本文适合对Handler有过了解,~~即使又忘了。~~但对网上的<<loop轮询转圈图>>有点印象的玩家。

前置概念


同步屏障消息

  1. 作用:系统使用的特殊的消息,可以看作优先处理异步消息的标识,当MessageQueue的队首是同步屏障消息时,忽略同步消息,一直执行最近的异步消息。通过postSyncBarrier/removeSyncBarrier增删同步屏障消息,非手动移除不会自动移除。
  2. 特点:target属性为空的Message就是同步屏障消息
  3. 事例:ViewRootImpl.scheduleTraversals 优先处理异步消息

IdleHandler

  1. 作用:闲时Handler,在没有消息或消息未到触发时机这样的闲时,执行的操作。
  2. 特点:是MessageQueue的静态接口,使用时复写boolean queueIdle()的方法执行闲时操作,返回值表示执行后是否保持存活状态。

epoll

Linux IO模式及 select、poll、epoll详解

正文


废话一下基本原理先

使用者通过Handler外部暴露的方法,向处于目标线程TLSLooper内的消息队列输入消息;

消息队列及时/延时地取出消息,并分发处理。以达到调度或延时地操作。

Handler通过MessageQueue.enqueueMessage(msg,when)入队消息

Looper.loop通过MessageQueue.next()出队消息

MessageQueue

MessageQueue的关键变量mMessages

消息队列实例,把消息根据触发时机早晚排列。具体代码表现为单链表的节点,代指队首(链表头)消息。

graph LR
1[A 延迟:1s]--next-->2[B 延迟:2s]--next-->3[C 延迟:3s]--next-->5[D 延迟:5s]

入队出对

截屏2021-04-23 上午2.10.15.png
  1. 入队方法enqueueMessage(),往队列存延迟触发的消息,并根据触发时间排好队。
  2. 出队方法next()一直死循环遍历队列,有到达触发时机的消息就取出消息。

阻塞/休眠

怎么能让入队消息的延迟触发呢?

先阻塞住next()方法,让其无法取消息。时间到了,在把阻塞恢复,取出消息即可。

队列内根本没消息,出队方法还一直死循环取消息,怎么办?

没消息也阻塞住next()方法,让其无法取消息。有新消息插入时,再通知他去取。
队内的下条消息还有很久才到触发时机:先阻塞。 队内的根本没有消息:一直休眠到有消息。
截屏2021-04-23 上午2.21.31.png 截屏2021-04-23 上午2.22.27.png

具体做法

next()的取消息死循环中用nativePollOnce(ptr, nextPollTimeoutMillis)阻塞/休眠。

  • 消息的触发时机未到时,阻塞到触发时机到为止;
  • 队列内一直没消息时,休眠直到有新消息入队,再用enqueueMessage()内的nativeWake(mPtr)唤醒。

拓展:可以不看

nativePollOnce传入的参数timeout通过JNI到Native层Looper::pollOnce->Looper::pollInner ->epoll_wait方法。

epoll_waitepoll_create创建的文件描述符A,去监听管道读取端文件描述符B的事件(使用epoll_ctl添加)。

  • timeout>0时,监听时长超过这个timeout仍没有事件就返回,中断阻塞。
  • timeout=-1,epoll_wait一直等待,直到新消息入队enqueueMessage()nativeWake(mPtr)在Native层向管道写入端写入“W”,触发监听中断阻塞。同时清空管道数据。

上边两种情形,都会给返回一种result,而pollOnce收到任何一种result都会退出。

epoll I/O复用机制是用一个文件描述符监听多个文件描述符的事件。

出队

nativePollOnce(ptr, nextPollTimeoutMillis)方法参数nextPollTimeoutMillis (即下个消息的延迟时间的)取值情况。

下个消息的延迟时间 消息队列内 阻塞情况
>0 延迟最近的消息,触发时机未到 阻塞到触发时机 释放cpu资源
=0 延迟最近的消息,触发时机到了 不阻塞
=-1 根本没消息 休眠到有消息 释放cpu资源

流程解读

next()出队方法,需要一个Message返回值。当nativePollOnce不再阻塞时,因为队列是按触发时机早晚排序的:

  1. 通常应该取队首消息;

  2. 但是队首是同步屏障消息时【Barrier1】,应该取触发时机最近的异步消息。

因此我们先取该msg,不管是队首还是最近异步,再判断是否应该将其返回和其他后续操作。

  • 当msg非空时【2】

    1. 如果msg触发时机到达【3`】,则返回msg。(当然返回前要整理一下队列)
    2. 如果msg触发时机未到【3】,则重新计算触发时间,然后将 nextPollTimeoutMillis 设为新时间,然后像下文"当msg为空时"一样,进行是否有IdleHandler及对其处理的操作。【4】/【5,6】
  • 当msg为空时【2`】,先将 nextPollTimeoutMillis 设为-1

    1. 如果也没有待处理的IdleHandler【4】:则跳出本次循环又回到nativePollOnce,此时nextPollTimeoutMillis=-1,阻塞至有新消息将其唤醒。
    2. 如果有待处理的IdleHandler:则遍历执行这些IdleHandler【5】(每次最多四个,执行其queueIdle回调),然后重置IdleHandler计数和nextPollTimeoutMillis=0完成本次循环【6】(nextPollTimeoutMillis=0让下次循环不再阻塞,以检查处理IdleHandler时是否又有新消息入队)。
Message next() {
    final long ptr = mPtr;/*MessageQueue 的native层地址*/
    if (ptr == 0) {//当消息循环已经退出,则直接返回
        return null;
    }
    int pendingIdleHandlerCount = -1; //待处理闲时handler数量
    int nextPollTimeoutMillis = 0;
    for (;;) {
        nativePollOnce(ptr, nextPollTimeoutMillis);//【1】阻塞:作用类似Java的 object.wait()
        synchronized (this) {
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;/*next()的返回值:此时为队头消息,即最近消息*/
            if (msg != null && msg.target == null) {//【Barrier1】如果队首是同步屏障消息,msg取最近的异步消息
		do {
                   prevMsg = msg;
                   msg = msg.next;
                } while (msg != null && !msg.isAsynchronous());//msg不是异步消息时,从队头至队尾遍历每个消息,直到msg为异步消息才推出遍历
            }            
            if (msg != null) {//【2】取到待处理的msg
                if (now < msg.when) {/*【3】时机未到:更新延迟时间*/
                    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                } else {/*【3`】处理msg的时机已到:取出msg,并整理队列*/
                    mBlocked = false;/*是否被阻塞:设为false供存消息时用*/
                    if (prevMsg != null) {/*【Barrier1`】若msg是因同步屏障循,而取出的最近的异步消息,改变指针指向跳过msg*/
                        prevMsg.next = msg.next;
                    } else {/* 取出msg,更新下一条消息为队首*/
                        mMessages = msg.next;
                    }
                    msg.next = null;//即将作为返回值,next变得没意义,置空。
                    return msg;/* 返回next消息*/
                }
            } else {/*【2`】消息为空,即没有消息了*/
                nextPollTimeoutMillis = -1;/*没有消息了,nextPollTimeoutMillis设为-1。线程阻塞*/
            }
            
            /*------------------------------空闲handler处理----------------------------------*/
            /* Idlehandles仅在队列为空或队首消息时机未到时才运行*/
            if (pendingIdleHandlerCount < 0
                    && (mMessages == null || now < mMessages.when)) {
                pendingIdleHandlerCount = mIdleHandlers.size();/*计算闲时任务量*/
            }
            if (pendingIdleHandlerCount <= 0) {
                mBlocked = true;/*【4】若经过计算上个if计算,连闲时Handler都没有,跳出本次循环*/
                continue;
            }
            if (mPendingIdleHandlers == null) {/*必有闲时任务待处理,否则上个if就continue出去了*/
                mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
            }
            mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
        }
        /*【5】必有闲时Handler需要遍历执行。连闲时Handler都没有的情况,在上文的if中continue出去。*/
        for (int i = 0; i < pendingIdleHandlerCount; i++) {
            final IdleHandler idler = mPendingIdleHandlers[i];
                //【5.1】执行IdleHandler的queueIdle方法,运行IdelHandler,例如处理日志上报 Gc等通过返回值由自己决定是否保持存活状态
               idler.queueIdle();
        }
	/*【6】执行完闲时Handler重置闲时计数和下次延迟时间*/
        pendingIdleHandlerCount = 0;
        // 因为执行闲时Handler(步骤【5】不在synchronized中)过程中,可能有新消息enqueue,需要重新检查。
        // 下次延迟时间置0,下次循环到步骤【1】时不阻塞。
        nextPollTimeoutMillis = 0;
    }
}

时序图

截屏2021-04-23 下午1.41.54.png

入队

关键变量mBlocked:源码上的注释翻译过来:出队方法next()是否被阻塞在pollOnce()处(nextPollTimeoutMillis≠0)。记录pollonce是否被阻塞的目的就是:是否需要唤醒

外部暴露操作方法的Handler类下,send(empty)Message/post +atTime/delay/AtFrontOfQueue等操作的最终归宿。 enqueueMessage(Message msg, long when):插入msg入队,when是自系统启动以来的非休眠运行时间(毫秒)。

流程解读

入队一个消息,流程参考存消息的情况图,并对照下边的代码。

情况【一】

队列为空、新消息是即时消息、新消息是延时最短消息时

入队的新消息插入到队头的情况:都需要nativeWake唤醒 出队的pollonce

a.队列内没消息 b.新入队的消息延时为0 c.新消息比队首的触发时机还早。与b类似
截屏2021-04-23 上午4.42.24.png 截屏2021-04-23 上午4.43.42.png
  1. 通过改变队首消息和新消息的next指针指向,把消息插入。
  2. 是否需要唤醒needWake = mBlocked, 这时候出队pollonce队列还没消息最近消息时机未到,还被阻塞mBlocked=true是必然的。然后nativeWake去唤醒pollonce去取刚存入的消息。

情况【二】

新消息不是上述的情况,不插入到队首,而是插入到队列中部。先查找位置再插入。

队头是同步屏障消息 且插入的消息是最近的异步消息 插入的消息不是最近的异步消息
截屏2021-04-23 上午5.03.37.png 截屏2021-04-23 上午5.04.16.png
是否需要唤醒 需要唤醒 不需要唤醒

除非队头是同步屏障消息,插入的消息是最近的异步消息,其他多数插入到队列中部的情况都不需唤醒

是否需要唤醒的条件needWake = mBlocked && p.target == null && msg.isAsynchronous();

  1. 出队pollonce处最近消息时机未到(经过上个if,队列现在非空),还被阻塞,mBlocked=true还是必然的。
  2. p.target == null 队首p的target为空符合同步屏障消息特点。
  3. msg.isAsynchronous() 新插入队列中部的消息是异步消息。

合起来唤醒条件就是:“队列内最近的消息触发时机未到,且队首消息是同步屏障消息时,新插入了一条异步消息”(还可能改变)。

然后再通过改变next指针指向,从队首至队尾遍历,查找合适的插入位置:

截屏2021-04-23 下午6.08.53.png

  1. (when < p.when)即新消息触发时机早于该位置的触发时机,插入位置找到,跳出遍历。
  2. p == null遍历到末尾,新消息的触发时机比队内的消息都晚,插入位置为队尾,跳出遍历。
  3. 查找插入位置的过程中。如果发现异步消息,则新消息虽异步,但不是离触发最近的,无需唤醒。因此唤醒条件更新为:队首是同步屏障消息时,新插入的消息为离触发最近的异步消息

最后改变指针指向,把消息插入到对应位置。

boolean enqueueMessage(Message msg, long when) {    
    synchronized (this) {/*可能有多个不同线程发消息*/
        msg.when = when;
        Message p = mMessages;// p 赋值为队首。根据触发时机when 来排序
        boolean needWake;
        if (p == null || when == 0 || when < p.when) {
          //【一】插入头部并唤醒:1、队列为空时 2、新消息延时为0是即时消息 3、新消息延时比队首的更短
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;/*出队方法next是否被阻塞在pollOnce()处(nextPollTimeoutMillis≠0)*/
        } else {//【二】消息插入MessageQueue中间,一般不需唤醒线程。除非队首同步屏障,且msg为!最近的!异步消息
            //【二a】队首是同步屏障消息,且插入的msg是异步消息。
            needWake = mBlocked && p.target == null && msg.isAsynchronous();
            Message prev;
            for (;;) {
                prev = p;
                p = p.next;
                /*prev、p 从队列的0、1 一直增至 last、null,来寻找msg合适的插入位置*/
                if (p == null /*last.next=null 插入到末尾*/|| when < p.when/*(队列的p.when越来越大1235,when=4)*/
                    break;
                }
                if (needWake && p.isAsynchronous()) {//【二b】插入的msg是异步消息是最近的
                    //在寻找msg插入位置过程中发现异步消息。说明msg前还有更早的异步消息。msg虽异步、但非最近。不需唤醒
                     needWake = false;
                }
            }
            /*经过循环确定插入位置,将入队的msg插入到prev与p中间 (3-5之间)*/
            msg.next = p; 
            prev.next = msg;
        }
        if (needWake) {
            nativeWake(mPtr);//【三】唤醒线程,nativePollOnce不在阻塞
        }
    }
    return true;
}

流程图

截屏2021-04-23 下午8.15.15.png

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们