美文网首页Android开发经验谈
Handler通信机制解析

Handler通信机制解析

作者: 刨坑 | 来源:发表于2022-03-31 14:08 被阅读0次

作者:bufan
转载地址:https://juejin.cn/post/7080781740028985358

Handler通信机制中,会涉及到四个相关类。

  • Handler:消息的发送者和接受者。
  • Message:消息本体。
  • MessageQueue:消息队列,存放和管理 Message
  • Looper:轮询器,循环从消息队列取消息并执行。

1. 初始化

1.1. Handler

Handler初始化的时候,通过 Looper.myLooper()获取 Looper对象,并且在判空的时候提示了 Looper的初始化方法是 Looper.prepare()。之后在通过 looper.mQueue获取到 MessageQueue对象,说明 Looper初始化的时候也初始化了 MessageQueue

然后是两个初始化参数: callback用于处理事件,也就是处理 Message,可以为空;async是异步消息标识,有了这个标识会将所有通过当前 Handler发送的消息标记为异步。

1.2. MessageQueue

MessageQueue的构造方法中,可以看到调用了 nativeInit()进行初始化,后面还可以看到调用底层方法来挂起和唤醒线程。

而这里的参数 qiutAllowed标记是否允许退出,比如主线程禁止退出,所以通过初始化时候的标志直接拦截掉退出操作。

1.3. Looper

Looper的构造方法是一个私有构造方法,在其中透传了允许退出的标志去创建了 MessageQueue,并且绑定当前线程。

Looper初始化是使用静态方法 prepare(),另外还有一个允许设置退出允许的私有静态方法和只供主线程调用的方法。

perpare(boolean quitAllowed)中,保证一个线程中只能创建一次 Looper,然后可以看到一个 perpareMainLooper()的方法,它是专门给主线程使用的,参数是禁止退出的,而其他线程初始化的参数是允许退出的。

最后还可以看到,获取当前线程和主线程 Looper的方法,两个方法一点不同的就是是否需要线程安全,因为需要获取主线程的 looper一般都是在子线程中调用的。

1.4. Message

Message的构造方法是一个空方法,可以通过这个方法创建 Message对象,但是更推荐通过 obtain()方法从缓存池中获取,其中提供了一系列的 obtain()方法用于初始化 Message的参数,其中的参数主要有:

  • whatint类型,用于区分不同的消息类型。
  • arg1arg2int类型,用于传输简单的数据。
  • objobject类型,传输任意类型的数据,跨进程传递需要是 Parcelable对象。
  • targetHandler类型,发送消息的 Handler对象,用于处理消息。
  • CallbackRunnable类型,用于处理消息。

1.5. HandlerThread

如果我们需要创建一个 Handler线程,不需要自己去做 Looper的初始化工作,直接使用 HandlerThread即可,它会在线程启动之后做好 Handler初始化的工作。

1.6. ActivityThread

主线程的 Hnadler由系统初始化,具体代码在 ActivityThreadmain方法中,这也是应用启动过程中克隆好新进程之后应用代码的入口,在其中调用专门的方法去初始化主线程的 Handler

1.7. 思考

这一部分分析了 Handler有关的几个类的初始化方法,并且看了一下系统做初始化工作的时候,都是调用 Looper.prepare()去初始化,一个的差异点就是主线程创建的是禁止退出的,而其他线程创建的都是允许退出的。

Looper.prepare()初始化的时候,同一个线程只能初始化一次,也就是说线程与 Looper之间是一对一的关系,而 MessageQueue的初始化也是在 looper初始化的过程中完成的,所以它和线程之间也是一对一的关系。再看 Handler的初始化方法中,通过 Looper.getLooper()获取当前线程的 Looper,再通过 Looper获得 MessageQueue,并且进行了绑定,也就说明 Hander 只能绑定到一个线程上面,但是反过来线程并不限制 Handler的数量,也就是一个线程可以绑定多个 Handler。最后是 Message在使用的过程中是只针对发送的线程的,但是在使用完成后可以进行回收,任意的线程都可以再次去使用它。

2. 消息处理

前面初始化的工作结束之后,接着来看整个消息的处理流程。

2.1. 消息发送

消息发送通常是调用 Handlerpost(Runnable r)或者 sendMessage(Message msg),它们两又有一系列的变形方法用于发送不同参数的消息,但是这些方法包装成 Message之后都会走到 enqueueMessage()中,在其中将 Message.target设置为当前的发送 Handler,并且如果 Handler是允许异步的话将 Message设置为异步消息。最后调用 MessageQueue的方法将消息插入消息队列中。

MessageQueue.enqueueMessage()方法将 Message存入消息队列中,在其中主要有三步:

  1. 调用 Message.markInUse(),标记消息已经被使用,如果再被拿去在别的地方使用会报错。
  2. 如果队列头为空,或者消息执行时间为零,或者消息执行时间小于表头执行时间,则将消息放在表头,并且根据 mBlocked的标志判断是否需要调用底层代码 nativeWake()唤醒线程。mBlocked标记的是当前线程已经调用 pollOnce()进入了等待。
  3. 如果上一步的条件不满足,则表明消息需要插在队列中间,找到执行时间对应位置插入。p.target == null是是否在同步屏障过程中的标识,前面提起过通过 Handler发送的消息最后都会设置 target,它为空的情况就是在同步屏障开启的时候插入队列的一条消息。然后 Message.isAsynchronous()标识当前消息是否是否是异步消息,那么这里唤醒的标识条件就是在同步屏障的时候插入的异步消息是第一条异步消息。

2.2. 消息处理

消息处理的起点是 Looper.loop()方法,前面有提及在 Handler启动的过程中都会调用它。它是一个死循环,重复的从 MessageQueue 中取消息,每取到一个消息就调用 target.dispatchMessage()处理消息,最后在处理完成之后调用 Message.recycleUnchecked()回收消息。

   public static void loop() {
        final Looper me = myLooper();
    。。。
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
        。。。
            try {
                msg.target.dispatchMessage(msg);   
            } 
            。。。
            msg.recycleUnchecked();
        }
    }

2.2.1. MessageQueue.next()

这个方法的作用就是就是从队列中取出一个消息,如果消息队列为空就将线程挂起等待。

方法的主体依旧是一个死循环,与 Looper的死循环不同,这里的循环是为了挂起被唤醒之后重新开始获取对应的 Message

循环开始的第一步就是调用 nativePollOnce(),它是一个挂起等待方法,第一次进入的时候等待的时间是零,也就是不会挂起。

然后就是读取 message,如果头部是正常的则取得就是头部的 message,如果 msg.target == null,前面说了它是是是否在同步屏障过程中的标识,就将 message替换消息队列中的第一条异步消息。也就是说,在同步屏障的情况下,拿的是第一个异步消息,不在同步屏障的情况下,拿的第一个同步消息。

接下来是处理这个 message,这里分为三个情况:

  1. 如果 message==null,表明消息队列中没有消息了,或者在同步屏障开启的时候没有异步消息了。就设置等待时间为负一,标识进入无限等待。
  2. 如果还没到这条消息的执行时间,就设置等待时间为它们的时间差。
  3. 其他情况下,就将当前消息直接返回,也就跳出了循环。

接着出现了另一个重要的参数 pendingIdleHandlerCount,它表示的就是需要执行的空闲任务数。首先在进入这个方法的时候它的值是负一,所以走到这里它可以去读取 mIdleHandlers.size(),不过它还有另一个条件,判断表头是否到了执行时间,也就是同步屏障开启的过程中,并不能去读取 size()

之后如果 pendingIdleHandlerCount还是小于零,就结束这次循环,而这个时候等待时间不可能为零,也就是说这个线程挂起等待了。

而如果 pendingIdleHandlerCount不小于零,就把所有需要执行的空闲任务拿出来执行。

在最后,将 pendingIdleHandlerCountnextPollTimeoutMillis设置为零。pendingIdleHandlerCount设置为零,也就禁止了下一次循环再次读取空闲任务,就说明在每一次进入到 next()方法的时候,空闲任务最多只有可能调用一次。而 nextPollTimeoutMillis设置为零,是因为空闲任务消耗了一些时间,需要重新读取消息队列或者重新计算等待时间。

2.2.2. Handler.dispatchMessage()

从队列中拿到消息之后,就走到了 Handler.dispatchMessage()中处理消息:

  1. 判断 message.callback,如果不为空就直接调用它去处理。
  2. 判断 Handler.mCallback,如果不为空就由它去处理。
  3. 调用 Handler的子类的 handleMessage()方法处理。

2.2.3. 设置同步屏障

前面多次提到了同步屏障,开启它是通过调用 MessageQueue.postSyncBarrier(),在其中创建了一个同步消息,而且没有为它设置 target。然后就是将它加入到消息队列的对应位置中。然后会继续执行对应的同步代码,直到这条没有 target的消息到达了表头,到这个时候就进入了同步屏障。

同步屏障开启之后并不会自动关闭,而是需要调用 MessageQueue.removeSyncBarrier()主动的去关闭它。在其中移除那个没有 target的消息,并且根据条件决定是否调用 nativeWake()唤醒线程。

2.3. 思考

这一部分介绍了 Handler如何完成一次消息的通信,发送消息是将消息存入到消息队列中,消息队列中的消息按照执行时间进行排序。而消息处理是在 Looper.loop()的死循环中进行的,虽然它是一个死循环,但它并不会一直在执行,因为通过 MessageQueue.next()获取消息的时候,会根据消息的情况做阻塞操作。

会阻塞的情况有:

  1. 下一条消息的执行时间还没到。
  2. 消息队列为空。
  3. 同步屏障的时候,消息队列中没有异步消息。

会唤醒的情况有:

  1. 设置阻塞的时间到了之后自动唤醒。
  2. 有一条消息放入表头,也就是它需要立即执行。
  3. 同步屏障开启的时候,有一条异步消息放入列表,并且它是第一条异步消息,也就是他需要立即执行。
  4. 同步屏障关闭的过程中,如果移除屏障消息之后的第一条消息不是屏蔽消息(多次开启同步屏蔽的时候会有多个屏蔽消息)。

最后,说了这么同步屏障,那么它是干什么的呢?通过代码逻辑就可以看出,在它开启的时候会跳过所有的同步消息,优先的去执行异步消息,而这么做的意义也就是改变优先级,有一些特别紧急的任务,需要现在放下手头的所有事情去做这件事情,防止同步消息太多导致它执行的太慢而产生一些问题,比如绘制过程中监听垂直同步信号会用到。

相关文章

网友评论

    本文标题:Handler通信机制解析

    本文链接:https://www.haomeiwen.com/subject/zdcnjrtx.html