该文章属于Android Handler系列文章,如果想了解更多,请点击Android Handler机制之总目录
前言
在前面的文章中,我们已经大概了解了ThreadLocal的内部原理,以及Handler发消息的大概流程。如果小伙伴如果对Handler机制不熟,建议阅读Android Handler机制之ThreadLocal与Android Handler机制之Handler 、MessageQueue 、Looper。该篇文章主要着重讲解Message的发送与取出的具体逻辑细节。在此之前,我们先回顾一下Handler发送消息的具体流程。
消息的发送
我们都知道当调用Handler发送消息的时候,不管是调用sendMessage,sendEmptyMessage,sendMessageDelayed还是其他发送一系列方法。最终都会调用sendMessageDelayed(Message msg, long delayMillis)方法。
1 | public final boolean sendMessageDelayed(Message msg, long delayMillis) |
该方法会调用sendMessageAtTime()方法。其中第二个参数是执行消息的时间,是通过从开机到现在的毫秒数(手机睡眠的时间不包括在内)+ 延迟执行的时间。这里不使用System.currentTimeMillis() ,是因为该时间是可以修改的。会导致延迟消息等待时间不准确。该方法内部会调用sendMessageAtTime()方法,我们接着往下走。
1 | public boolean sendMessageAtTime(Message msg, long uptimeMillis) { |
这里获取到线程中的MessageQueue对象mQueue(在构造函数通过Looper获得的),并调用enqueueMessage()方法,enqueueMessage()方法最终内部会调用MessageQueue的enqueueMessage()方法,那现在我们就直接看MessageQueue中把消息加入消息队列中的方法。
消息的加入
当通过handler调用一系列方法如sendMessage()、sendMessageDelayed()方法时,最终会调用MessageQueue的enqueueMessage()方法。现在我们就来看看,消息是怎么加入MessageQueue(消息队列)中去的。
1 | boolean enqueueMessage(Message msg, long when) { |
这里大家肯定注意到了nativeWake()方法,这里先不对该方法进行详细的描述,下文会对其解释及介绍。
其实通过代码大家就应该发现了,在将消息加入到消息队列中时,已经将消息按照等待时间进行了排序。排序分为两种情况(这里图中的message.when是与当前的系统的时间差):
第一种:如果队列中没有消息,或者当前进入的消息比消息队列中头部的消息等待时间短,那么就放在消息队列的头部
第二种:反之,循环遍历消息队列,把当前进入的消息放入合适的位置(比较等待时间)
综上,我们了解了在我们使用Handler发送消息时,当消息进入到MessageQueue(消息队列)中时,已经按照等待时间进行了排序,且其头部对应的消息是Loop即将取出的消息。
获取消息
我们都知道消息的取出,是通过Loooper.loop()方法,其中loop()方法内部会调用MessageQueue中的next()方法。那下面我们就直接来看next()方法。
1 | Message next() { |
这里大家直接看MessageQueue的next()法肯定会很懵逼。妈的,这个nativePollOnce()方法是什么鬼,为毛它会阻塞呢?这个msg.isAsynchronous()判断又是怎么回事?妈的这个逻辑有点乱理解不了啊。大家不要慌,让我们带着这几个问题来慢慢分析。
Native消息机制
其实在Android 消息处理机制中,不仅包括了Java层的消息机制处理,还包括了Native消息处理机制(与我们知道的Handler机制一样,也拥有Handler、Looper、MessageQueue)。这里我们不讲Native消息机制的具体代码细节,如果有兴趣的小伙伴,请查看—–>深入理解Java Binder和MessageQueue
这里我们用一张图来表示Native消息与Jave层消息的关系(这里为大家提供了Android源码,大家可以按需下载),具体细节如下图所示:
(这里我用的别人的图,如有侵权,请联系我,马上删除)。
其实我们也可以从Java层中的MessageQueue中几个方法就可以看出来。其中声明了几个本地的方法。
1 | private native static long nativeInit(); |
特别是在MessageQueue构造方法中。
1 | MessageQueue(boolean quitAllowed) { |
在Java层中MessageQueue在初始化的时候,会调用本地方法去创建Native MessageQueue。并通过mPrt保存了Native中的MessageQueue的地址。
Native消息机制与Java层的消息机制有什么关系
想知道有什么关系,我们需要查看frameworks\base\core\jni\android_os_MessageQueue.cpp文件,
1 | static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) { |
其实nativeInit()方法很简单,初始化NativeMessageQueue对象然后返回其地址。现在我们继续查看NativeMessageQueue的构造函数。
1 | NativeMessageQueue::NativeMessageQueue() : |
哇,我们看见了我们熟悉的”Looper“,这段代码其实很好理解。Native Looper调用静态方法getForThread(),获取当前线程中的Looper对象。如果为空,则创建Native Looper对象。这里大家肯定会有个疑问。当前线程是指什么线程呢?想知道到底绑定是什么线程,我们需要进入Native Looper中查看setForThread()与getForThread()两个方法。
getForThread()从线程中获取设置的变量
1 | /** |
这里pthread_getspecific()机制类似于Java层的ThreadLocal中的get()方法,是从线程中获取key值对应的数据。其中通过我们可以通过注释就能明白,Native Looper是存储在本地线程中的,而对应的线程,就是调用它的线程,而我们是在主线程中调用的。故Native Looper与主线程产生了关联。那么相应的setForThread()也是对主线程进行操作的了。接着看setForThread()方法。
setForThread()从线程中设置变量
1 | /** |
这里pthread_setspecific()机制类似于Java层的ThreadLocal中的set()方法。通过注释我们明白将Native looper放入调用线程,如果已经存在,就替换。如果为空就删除。
nativePollOnce()方法为什么会导致主线程阻塞
经过上文的讨论与分析,大家现在已经知道了,在Android消息机制中不仅有 Java层的消息机制,还有Native的消息机制。既然要出里Native的消息机制。那么肯定有一个处理消息的方法。那么调用本地消息机制消息的方法必然就是nativePollOnce()方法。
1 | static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj, |
在nativePollOnce()方法中调用nativeMessageQueue的pollOnce()方法,我们接着走。
1 | void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) { |
这里我们发现pollOnce(timeoutMillis)内部调用的是Natave looper中的 pollOnce(int timeoutMillis, int outFd, int outEvents, void** outData)方法。继续看。
1 | int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) { |
由于篇幅的限制,这里就简单介绍一哈pollOnce()方法。该方法会一直等待Native消息,其中 timeOutMillis参数为超时等待时间。如果为-1,则表示无限等待,直到有事件发生为止。如果值为0,则无需等待立即返回。 那么既然nativePollOnce()方法有可能阻塞,那么根据上文我们讨论的MessageQueue中的enqueueMessage中的nativeWake()方法。大家就应该了然了。nativeWake()方法就是唤醒Native消息机制不再等待消息而直接返回。
nativePollOnce()一直循环为毛不造成主线程的卡死
到了这里,其实大家都会有个疑问,如果当前主线程的MessageQueue没有消息时,程序就会便阻塞在loop的queue.next()中的nativePollOnce()方法里,一直循环那么主线程为什么不卡死呢?这里就涉及到Linux pipe/epoll机制,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。–摘自Gityuan知乎回答
如果大家想消息了解Native 消息机制的处理机制,请查看—–>深入理解Java Binder和MessageQueue
屏障消息与异步消息
屏障消息
在next()方法中,有一个屏障的概念(message.target ==null为屏障消息),如下代码:
1 | if (msg != null && msg.target == null) { |
其实通过代码,当出现屏障的时候,会滤过同步消息,而是直接获取其中的异步消息并返回。如下图所示:
在Hadnler无参的构造函数中,默认设置的消息都是同步的。那我们就可以知道在Android中消息分为了两种,一种是同步消息,另一种是异步消息。在官方的解释中,异步消息通常代表着中断,输入事件和其他信号,这些信号必须独立处理,即使其他工作已经暂停。
异步消息
要设置异步消息,直接调用Message的setAsynchronous()方法,方法如下:
1 | /** |
大家可以看到,设置异步消息,官方文档对其有详细的说明,侧面体现出了异步消息的重要性。那下面我就带着大家一起来理一理官方的注释说明。
- 如果设置了异步消息,异步消息将不会受到屏障的影响(从next()方法中,我们已经了解了,当出现屏障的时候,同步消息会直接被过滤。直接返回最近的异步消息)
- 在某些操作中,例如视图进行invalidation(视图失效,进行重绘),会引入屏障消息(也就是将message.target ==null的消息放入消息队列中),已防止后续的同步消息被执行。同时同步消息的执行会等到视图重绘完成后才会执行。
有哪些操作是异步消息呢
这里我就直接通过ActivityThread中的几个异步消息给大家做一些简单介绍。这里我就不用代码展示了,用图片来表示更清晰明了。
在ActivityThread中,有一个sendMessage()多个参数方法。我们明显的看出,有四个消息是设置为异步消息的。DUMP_SERVICE、DUMP_HEAP、DUMP_ACTIVITY、DUMP_PROVIDER。从字面意思就可以看出来。回收service、回收堆内存、回收Activity、回收Provider都属于异步消息。
屏障消息发送的时机
那么Android中在哪些情况下会发生屏障消息呢?其实最为常见的就是在我们界面进行绘制的时候,如在ViewRootImpl.scheduleTraversals()中。
1 | void scheduleTraversals() { |
在调用scheduleTraversals()方法时,我们发现会发生一个屏障过去。具体代码如下:
1 | private int postSyncBarrier(long when) { |
这里我们直接将围栏放在了消息队列中,同时重要的是我们并没有直接设置target,也就是tartget =null。其实现在我们可以想象,我们当我们正在进行界面的绘制的时候,我们是不希望有其他操作的,这个时候,要排除同步消息操作,也是可能理解的。
IdleHandler(MessageQueuqe空闲时执行的任务)
在MessageQueue中的next()方法,出现了IdleHandler(MessageQueuqe空闲时执行的任务),查看MessageQueue中IdleHander接口的说明:
1 | /** |
当线程正在等待更多消息时,会回调该接口,同时queueIdl()方法,会在消息队列执行完所有的消息等待且在等待更多消息时会被调用,如果返回true,表示该任务会在消息队列等待更多消息的时候继续执行,如果为false,表示该任务执行完成后就会被删除,不再执行。
其中MessageQueue通过使用addIdleHandler(@NonNull IdleHandler handler) 方法添加空闲时任务。具体代码如下:
1 | public void addIdleHandler(@NonNull IdleHandler handler) { |
既然MessageQueue可以添加空闲时任务,那么我们就看看最为明显的ActivityThread中声明的GcIdler。在ActivityThread中的H收到GC_WHEN_IDLE消息后,会执行scheduleGcIdler,将GcIdler添加到MessageQueue中的空闲任务集合中。具体如下:
1 | void scheduleGcIdler() { |
ActivityThread中GcIdler的详细声明:
1 | //GC任务 |
GcIdler方法理解起来很简单、就是获取上次GC的时间,判断是否需要GC操作。如果需要则进行GC操作。这里ActivityThread中还声明了其他空闲时的任务。如果大家对其他空闲任务感兴趣,可以自行研究。
什么时候唤醒主线程呢
通过上文的了解,大家已经知道了Native的消息机制可能会导致主线程阻塞,那么唤醒Native消息机制(让Native消息机制不在等待Native消息,也就是nativePollOnce()方法返回)在整个Android的消息机制中尤为重要,这里放在这里给大家讲是因为唤醒的条件涉及到屏障消息与空闲任务。大家理解了这两个内容后再来理解唤醒的时机就相对容易一点了,这里我们分别对唤醒的两个时机进行讲解。
在添加消息到消息队列中
1 | boolean enqueueMessage(Message msg, long when) { |
上述代码,我们很明显的看见Native消息机制的唤醒,受到needWake这个变量影响,needWake ==true是在两个条件下。
- 第一个:如果当前消息按照等待时间排序是在消息队列的头部, needWake = mBlocked,且mBlocked会在当前消息队列中没有消息可以处理,且没有空闲任务的条件下为true(mBlocked变量的赋值会在下文讲解)。
- 第二个:如果当前mBlocked=true(第一个条件判断),且消息队列头部消息是屏障消息,同时当前插入的消息为异步消息的条件。needWake = true。
在空闲任务完成的时候唤醒
1 | Message next() { |
这里我们可以看到 mBlocked = true的条件是在消息队列中没有消息可以处理,且也没有空闲任务的情况下。也就是当前mBlocked = true会影响到MessageQueue中enqueueMessage()方法是否唤醒主线程。
如果当前空闲任务完成后,会将nextPollTimeoutMillis 置为0,如果nextPollTimeoutMillis =0,会导致nativePollOnce直接返回,也就是会直接唤醒主线程(唤醒Native消息机制层)。
MessageQueue取出消息整体流程
到目前为止,大家已经对整个消息的发送与取出有一个大概的了解了。这里我着重对MessageQueue取消息的流程画了一个简单的流程图。希望大家根据对取消息有个更好的理解。
总结
- Handler在发消息时,MessageQueue已经对消息按照了等待时间进行了排序。
- MessageQueue不仅包含了Java层消息机制同时包含Native消息机制
- Handler消息分为异步消息与同步消息两种。
- MessageQueue中存在“屏障消息“的概念,当出现屏障消息时,会执行最近的异步消息,同步消息会被过滤。
- MessageQueue在执行完消息队列中的消息等待更多消息时,会处理一些空闲任务,如GC操作等。
感谢
站在巨人的肩膀上。可以看得更远。该篇文章参阅了一下几本图书与源码。我这里我给了百度云盘的下载链接。大家可以按需下载。