Android Handler机制彻底梳理

Android的消息机制其实也就是Handler相关的机制,对于它的使用应该熟之又熟了,而对于它的机制的描述在网上也一大堆【比如15年那会在网上抄了一篇https://www.cnblogs.com/webor2006/p/4837623.html对它的关系描述,但仅仅是背一背概念】,在面试时也时不时的会问起它,说实话从事Android这么多年也没自己从头到尾的去将它的工作机制详细的给挼一遍,所以这里写一篇关于它的整个机制的描述来加深对Handler的核心机制的进一步了解。

Android消息机制:

先来看一张关于整个消息机制的描述图,这个流程会在之后自己从0开始手写实现的,如下:

以上模型大致解释一下:

1、以Handler的sendMessage方法为例,当发送一个消息后,会将此消息加入消息队列MessageQueue中。
2、Looper负责去遍历消息队列并且将队列中的消息分发给对应的Handler进行处理。
3、在Handler的handleMessage方法中处理该消息,这就完成了一个消息的发送和处理过程。
这里从图中可以看到参与消息处理有四个对象,它们分别是 Handler, Message, MessageQueue,Looper。

其中在图中涉及到这两个状态:

这个在之后的源码分析中是能看到的。

ThreadLocal的工作原理:

ThreadLocal 是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有再指定线程中可以获取到存储的数据,对于其他线程来说则无法获取到数据。为啥要先说它呢?因为打好这个基础后有助于分析Android的消息机制,下面先举例来对它的工作原理有一个了解:

先来瞅一下ThreadLocal的源码:

接受一个泛型,那该泛型是怎么用的呢?

它里面有一个ThreadLocalMap的静态内部类,然后在我们往ThreadLocal存东西时最终的这个T会赋值给ThreadLocal中的Entry类中的value,如下:

 

 

下面来定义一下:

下面来调用一下:

此时看一下这个get()方法的实现:

接下来根据当前线程来获取ThreadLocalMap:

 

其中可以看一下该threadLocals在Thread初始化的情况:

所以。。回到get()主流程上来:

然后:

最后setInitialValue()方法就会返回cexo,然后整个get()方法就返回了:

这就是为啥我们的结果显示cexo的原因,好,下面将在子线程中再来打印一下:

其原因就不多分析了,跟在主线程的是一模一样的流程,都是由于我们重写了initialValue()方法。 

下面再来看:

此时再来分析一下set的过程:

然后再拿时:

下面再来改一下程序,再改之前需要将ThreadLocal中设置的东东给清除掉,避勉内存泄漏,如下:

好,再来新建一个线程:

也就是通过ThreadLocal存放的值是跟线程绑定的,关于它的大致使用就了解到这,下面正式进入到核心的消息机制源码分析阶段了。

Android消息机制源码分析:

1、启动App创建全局唯一得Looper对象和全局唯一得MessageQueue消息对象:

先来看一下整体这块的流程图:

以此为蓝本,接下来分析一下这块流程的源码【以Android9.0为例】:

点击进入看一下:

看到了ThreadLocal的身影了,这就是为啥要先了解它的机制的原因之所在,好,此时流程就到了这:

然后看一下这个Looper创建的细节,就会创建全局唯一的消息队列,如下:

以上就是在主线程启动时创建Looper的大致过程。

2、Activity中创建Handler:

先贴一下整个这块的流程:

下面先回顾一下它的实际使用,比较简单,主要是根据实际的应用来过行源码底层分析会比较亲切:

 

先来看一下Handler()的构造:

 

3、发送消息:

整体流程:

咱们依照此流程来分析一下:

继续往里跟:

流程就跑到了这:

接下来来看一个这个enqueueMessage()方法:

其中可以看一下Message.target变量:

也就是每个消息都绑定了Handler,下面回到主流程:

也就是如流程图的这一步:

具体看一下在全局消息队列中的处理:

 呃,貌似有点颠覆对队列的认知,不应该拿到消息往队列中插么,貌似这里就是简单的给它里面的成员变量赋了个值而已呢,是的,这个消息队列一定得要知道并非是我们认知中的那种,下面看一眼它的javadoc对它的描述:

以上是消息发送的大致流程。

4、处理消息:

先来看一下这块的大致流程图:

依据它再来看一下代码流程:

下面具体来看一下该loop()方法,首先也是获取全局唯一的Looper和MessageQueue对象:

接着则会循环从队列中取消息,将会调用消息队列绑定的Handler的相关的方法来对消息进行处理,如下:

那咱们再来看一下Handler中的这个消息分发是如何来处理该消息的:

 

消息阻塞和延时:

Looper 的阻塞主要是靠 MessageQueue 来实现的,在next()@MessageQuese 进行阻塞,在 enqueueMessage()@MessageQueue 进行唤醒。主要依赖 native 层的 Looper 依靠 epoll 机制 进行的。
nativePollOnce(ptr, nextPollTimeoutMillis); 这里调用naive方法操作管道,由nextPollTimeoutMillis决定是否需要阻塞
nextPollTimeoutMillis为0的时候表示不阻塞,为-1的时候表示一直阻塞直到被唤醒。

消息阻塞流程:

下面具体来看一下代码,先看在Looper中的loop()的阻塞相关:

接着则看MessageQueue的next()方法了:

那假如在这块阻塞了之后,那在主线程中不会引发ANR么?其实是不会的,原因简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制。

好,下面再详细的来分析其阻塞的一个整体流程:

假设loop for循环第一次、MessageQueue for循环也第一次:

 

接着就会往下执行,则会执行到这:

好继续:

 

假如它现在等于0,则会执行到这了:

 此时再下一次循环中,则就会进入阻塞状态了:

而假如mIdleHandlers.size()>0,那么执行顺序就会发生变化了,如下:

 

好,接下来再假设mMessages不为null:

当退出循环时,如果找到了,则走如下条件分支:

下面具体再看下里面的条件:

而这个流程往下:

这就是消息延时的一个机制。 

消息延时入队流程:

此时就需要回到MessageQueue中的enqueueMessage()方法了,如下:

而如果当前消息的when要大于上一条消息的when,则会走另一个条件分支了,如下:

先来看一下javadoc的说明:

而在理解这个分支代码之前需要理解一个东东:

对于Message对像池的大小是有大小的,那是多少呢?下面看下源码的定义:

 

那如果消息大小超过了这个对象池总个数呢,则是插不进去的,具体这块的代码如下:

 

其中sPool是一个静态变量:

了解了它的对象池之后,下面再回过头来理解这个条件:

比如上一个消息为:

然后再插入个新消息为:

此时进入条件时:

也就是处理完之后就成这样了:

同样的如果再来第三个msg:

同样的也会按从小到大的顺序来进行排序:

 

最后则会执行唤醒的条件,如下:

 

当执行唤醒时,则在next()中正在阻塞的就会被唤醒:

手写Handler消息核心机制:

经过这么大的篇幅来对Handler核心流程的源码进行了分析之后,接下来弄一个比较有“挑战”的事,从0开始手写一上handler发送及消息接收的“核心流程”,不涉及到延时相关的东东,因为那块太复杂了,下面从ActivityThread.main()中一直到Activity创建消息到接收消息手写实现一下,这里抛开Android环境以单元测试的方式来手写,先定义一个main()方法:

来模拟它:

然后我们先将Looper、Handler、MessageQueue、Message都创建一下:

好,在main()中首先得生成全局唯一的Looper对象,如下:

 

接下来实现这个方法:

校仿一下:

然后在Looper的构造方法中需要初始化MessageQueue,如原码中所示:

所以继续校仿:

好,接下来再来创建Handler,如这个流程:

它里面持有Looper和MessageQueue的引用,如系统源码所示:

所以咱们在我们的构造方法中来实例化一下:

然后来在MyLooper中实现myLooper()方法,还是校仿源码:

然后再来实例化MessageQueue:

然后我们在主线程中来创建一个Handler:

我们知道在实际使用时需要重写它里面的一个handleMessage()方法,所以咱们还得在MyHandler中来定义一下该方法,如下:

此时就可以重写方法了:

好,接下来在子线程中来发送消息,如这个流程:

其中消息里面得要有一些属性,这里只定义简单的几个,如下:

然后咱们继续来创建消息:

此时咱们再来定义发送消息的方法,先看一下源码是如何写的:

所以咱们也来写一下:

我们之前分析过MessageQueue的入队方法,它是采用对像池的方式来存储的,咱们这里简单一点,直接用阻塞队列来存放,重在模拟整个过程,如下:

好,最后就是开始Looper的消息循环了,如源码所示:

接下来这就是最后一步的实现了,下面来实现一下:

好,接下来则来看一下这个next()方法的实现:

这里就木有实现阻塞队列,还是重点看整体流程,好继续,先看一下系统源码,当拿到消息之后接下来是怎么处理的:

所以咱们校仿一下:

接下来就要分发消息的处理了,还是先来参照系统的方式:

 

所以咱们简单处理一下:

修改一下:

好,接下来就到最后的消息处理啦,如下:

Handler中常见问题分析:

  • 为什么不能在子线程中更新UI,根本原因是什么?

    mThread是UI线程,这里会检查当前线程是不是UI线程。那么为什么onCreate里面没有进行这个检查呢。这个问题原因出现在Activity的生命周期中,在onCreate方法中,UI处于创建过程,对用户来说界面还不可视,直到onStart方法后界面可视了,再到onResume方法后界面可以交互。从某种程度来讲,在onCreate方法中不能算是更新UI,只能说是配置UI,或者是设置UI的属性。这个时候不会调用到ViewRootImpl.checkThread(),因为ViewRootImpl没被创建。而在onResume方法后,ViewRootImpl才被创建。这个时候去交互界面才算是更新UI。
    setContentView只是建立了View树,并没有进行渲染工作(其实真正的渲染工作是在
    onResume之后)。也正是建立了View树,因此我们可以通过findViewById()来获取到View对象,但是由于并没有进行渲染视图的工作,也就是没有执行ViewRootImpl.performTransversal。同样View中也不会执行onMeasure(),如果在onResume()方法里直接获取View.getHeight()/View.getWidth()得到的结果总是0。

  • 为什么主线程用Looper死循环不会引发ANR异常?

    简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,
    此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,
    通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制。

  • 为什么Handler构造方法里面的Looper不是直接new?
    如果在Handler构造方法里面new Looper,怕是无法保证保证Looper唯一,只有用Looper.prepare()才能保证唯一性,具体去看prepare方法。
  • MessageQueue为什么要放在Looper私有构造方法初始化?
    因为一个线程只绑定一个Looper,所以在Looper构造方法里面初始化就可以保证mQueue也是唯一的Thread对应一个Looper 对应一个 mQueue。
    谈到这点,发现咱们手写的代码中关于Looper的构造定义不对,当时是定义成了public了,如下:

    而系统中定义确实是私有的:

    所以修改一下:

  • Handler.post的逻辑在哪个线程执行的,是由Looper所在线程还是Handler所在线程决定的?
    由Looper所在线程决定的。逻辑是在Looper.loop()方法中,从MsgQueue中拿出msg,并且执行其逻辑,这是在Looper中执行的,因此由Looper所在线程决定。
  • MessageQueue.next()会因为发现了延迟消息,而进行阻塞。那么为什么后面加入的非延迟消息没有被阻塞呢?
    这是因为新消息在入列时,会存在唤醒的情况,如下:
  • Handler的dispatchMessage()分发消息的处理流程?

    Msg.callback 在mHandler1.post()中使用
    mCallback在new Handler是通过接口回调

    Post()和sendMessage()都是发送消息,加入消息队列得方式也是一样,区别在于处理消息得方式。通过跟踪源码,容易区分。

终于。。整个Handler相关的东东都梳理完了,真的,还是细节挺多的,不过这么走了一遍真的受益匪浅!!如果把整个全部消化,我想未来不管面试官怎么来问Android的消息机制都会非常轻松的面对!!!

posted on 2019-10-07 15:28  cexo  阅读(1728)  评论(0编辑  收藏  举报

导航