Android的消息机制其实也就是Handler相关的机制,对于它的使用应该熟之又熟了,而对于它的机制的描述在网上也一大堆【比如15年那会在网上抄了一篇对它的关系描述,但仅仅是背一背概念】,在面试时也时不时的会问起它,说实话从事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的消息机制都会非常轻松的面对!!!