Android 消息机制(一)消息队列的创建与循环的开始 Looper与MessageQueue
写在前面
本文基于Android 7.1.1 (API 25)
的源码分析编写
与之前的触摸事件分发机制分析的文章一样,Android
系统机制的分析中关键的一环就是事件消息的处理。之前也说过,Android
本质上是一个事件驱动的模型,通过各式各样不断产生事件消息的来推动UI、数据的更新与对我们交互的反馈,没有事件消息的产生,就不会有直观的界面的变化,也就不会有应用丰富的功能。
所以Android
的消息机制与其他过程的关系是极其紧密的,例如启动Activity
的过程就涉及到ActivityManagerService
与应用主进程的通信,产生的通知消息通过Binder
机制送入应用主进程的消息队列,再由主进程的消息循环来读取这一消息来进行处理。之前触摸事件分发中也是利用了应用主进程的消息队列来读取我们的触摸事件再进行后续的分发处理。可以说消息队列在各种通信过程中无处不在。
消息队列的存在为异步处理提供了一个非常好的基础,有了消息队列之后,我们就可以在新的线程中处理计算、IO密集、阻塞的任务而不会影响UI的更新,在处理过程中可以通过向消息队列中放入消息来进行UI的更新操作,而发送消息的行为也避免了工作线程为了等待返回而造成的阻塞。
可以说,想要了解其他基于事件的过程,对主线程消息机制的了解是必不可少的基础,在触摸事件分发机制分析的文章中我对消息机制还不是很了解,所以后来发现分析中有很多描述不妥的地方,所以在对消息机制的系统学习之后我又修改完善了这部分的内容。
引入
Android Studio
的3.0
版本中引入了一个强大的性能分析工具:Android Profiler
,对于它的详细介绍可以看官方的文档。
我们对一个简单的HelloWorld
应用进行方法分析:
可以看到,对于这样一个没有任务需要处理的程序,这段时间中它一直执行的是nativePollOnce()
方法,对于这个,stackoverflow
上就有人提了一个问题。这个方法其实就是消息队列在队列中没有消息时处于等待状态执行的一个Native
方法。
我们的分析就从消息队列(MessageQueue)与负责执行循环过程的Looper
对象的创建与开始运行开始。
Looper与MessageQueue的创建
当一个Activity
被创建时,ActivityThread
的main()
方法会被执行(关于Activity
创建过程的内容,请参阅启动分析相关的文章):
1 | public static void main(String[] args) { |
Looper的创建
第5行中,调用Looper
的prepareMainLooper()
方法来创建Looper
对象:
1 | public static void prepareMainLooper() { |
1 | private static void prepare(boolean quitAllowed) { |
这里的sThreadLocal
对象的类型是ThreadLocal<Looper>
是一个存放在TLS
(Thread-local storage)中的对象容器,储存在其中的对象的特点是每个线程中只有一个,并且个线程中储存的该对象不相同。
我们在这里新建了一个Looper
对象并放入了TLS
中:
1 | private Looper(boolean quitAllowed) { |
mThread
保存了当前运行Looper
的进程信息。
而mQueue
就是与Looper
对应的MessageQueue
。
MessageQueue的创建
1 | MessageQueue(boolean quitAllowed) { |
构造函数十分简单,除了初始化quitAllowed
标记之外,就是对mPtr
的初始化了。
那么mPtr
是什么呢?可以推测出的是,真正的MessageQueue
的创建一定在nativeInit
这个Native
调用中,也就是说,我们的MessageQueue
实际上存在于Native
层。
android_os_MessageQueue.cpp
:
1 | static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) { |
在Native
层创建了一个NativeMessageQueue
对象:
1 | NativeMessageQueue::NativeMessageQueue() : |
这里做的事情可以和Java
层进行对应:在TLS
中创建了一个Looper
对象,但这个Looper
对象和Java
层并不是同一个,并且他们的功能也不相同:Java
层的Looper
是为了处理的消息队列中的消息,Native
中的Looper
是为了处理注册的自定义Fd
引起的Request
消息,这些消息一般来自于系统底层如触摸事件等(这个部分另开文章讲,这篇文章只关注一般的事件分发)。
我们来看看这个与NativeMessageQueue
对应的Native
Looper
的构造:
Native Looper 创建与 Epoll的初始化
1 | Looper::Looper(bool allowNonCallbacks) : |
这里就要涉及一些Linux
中系统调用中eventfd()
函数与多路I/O复用函数epoll()
的相关知识了,这部分也是消息机制的底层核心。
上面的代码中第5行中使用eventFd()
系统调用获取了一个mWakeEventFd
作为后续epoll()
用于唤醒的File Descriper
(文件描述符)。这里是较之前版本有所不同的地方,网上找到的大部分分析文章中的这个地方还是使用的之前使用的管道机制,也就是通过pipe()
系统调用来创建一对Fd
,再利用这对Fd
来进行监听唤醒操作。相比于管道,Linux
在内核版本2.6.22引入的eventFd
在解决这种简单的监听操作中的开销比较小,并且更加轻量。
我们现在有了一个eventFd
对象的Fd
,下面我们进入第10行的rebuildEpollLocked()
调用:
1 | void Looper::rebuildEpollLocked() { |
第3行中,进行了系统调用epoll_create()
初始化了一个epoll
实例。之后的6-9行创建了epoll
注册需要使用的eventItem
并设置了events
属性与fd
域。
在第10行,进行了系统调用epoll_ctl()
来将之前创建的mWakeEventFd
与eventItem
注册到epoll
。那么这些步骤的目的是什么呢?
简单地说,epoll
这个系统提供的组件允许我们对多个文件描述符(fd
)进行监听,注册监听的fd
后,可以调用epoll_wait()
函数,当fd
所指向的对象的数据可用时,epoll_wait()
函数就会返回,同时以events
的形式返回发生改变的fd
对应的eventItem
。
借助这个功能,我们就可以实现在没有事件的时候让线程阻塞,当新的事件来临的时候让线程解除阻塞并唤醒。
到这里你可能会想,这样的功能使用一个标志量,不断地查询这个标志量,当标志量发生变化的时候唤醒不也可以实现相同的功能吗?为什么要使用这么复杂的机制呢?这是因为Looper
同时为我们提供了addFd()
函数让我们可以设置自定义的fd
与对应的event
,然后在Native Looper
中对自定义的fd
发生改变的事件进行处理(上面代码中后面的部分就是在处理这部分注册)。之前文章讲过的触摸事件分发就是这样做的。(再次说明这一剖分的内容另外一篇文章讲,本篇只涉及一般的消息处理机制)
现在,注册epoll
的过程已经完成,Native Looper
的初始化也到此结束。
现在我们回到nativeInit
调用,它的返回值赋给了Java
层MessageQueue
的mPtr
域:
1 | static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) { |
第9行就将创建的NativeMessageQueue
对象的地址转换为一个Java long
类型返回,之后调用Native
方法的时候就会传入这个参数来找到这个MessageQueue
。
用一张图来梳理这个过程:
消息循环
loop()
初始化过程结束后,我们回到ActivityThread
的main()
函数:
1 | Looper.loop(); |
调用了Looper
的loop()
函数开始消息循环。
1 | public static void loop() { |
省略掉一些log
的代码之后,我们看到第7行开始了一个无限循环,循环的第一步就是从MessageQueue
里面获取一条Message
,后面有一个注释告诉我们这个调用可能会阻塞。我们先不管这个调用具体情况,假设我们从这个调用中返回,我们先看后面的处理过程。
首先检查获取到的msg
是否为null
,如果为null
,那么将会直接退出loop()
函数,对于Activity
的主线程来说,这个情况只会发生在应用退出的时候。
下面就直接调用了Message
的target
的dispatchMessage()
函数,在使用Handler
来发送消息的时候,这个target
指的就是Handler
本身,后面会看到这个过程。
MessageQueue next()
这个函数过程比较长, 我们分开来分析。
1 | Message next() { |
第一部分是变量的初始化,如果MessageQueue
的mPtr
为0的话,说明NativeMessageQueue
没有正确初始化,返回null
结束消息循环。
下面定义了两个变量,第一个pendingIdleHandlerCount
初始化为-1,它表示的是将要执行的空闲Handler
数量,之后会用到。
第二个nextPollTimeoutMillis
就是距下一条消息被处理需要等待的时间。
下面又进入了一个无限循环,注意第11行,我们看到了熟悉的调用,它是引入中讲的在没有事件处理的时候不断执行的函数。我们可以猜测,等待的过程就是发生在这个函数中的。
我们同样先看下面的处理,现在只要知道这个函数会造成阻塞,当有新的Message
或者达到超时时间时才会返回,这点非常重要。
下面的过程:
1 | synchronized (this) { |
这个过程比较简单,需要注意的就是Message
的链表结构,每次取首元素来进行处理。
1 | // 只会在第一次没有消息的时候执行,检查mIdleHandlers中注册的IdleHandler |
这里的mIdleHandlers
中注册了一些需要在没有消息处理时进行的任务,在处理这些任务的过程中使用了pendingIdleHandler
作为临时容器。这个过程就是去执行这些IdleHandler
的过程。
现在我们看完了返回消息的全过程,其中只有一环没有解决了:nativePollOnce
NativePollOnce()
调用的是Native
层android_os_MessageQueue.cpp
下的函数:
1 | static void android_os_MessageQueue_nativePollOnce(JNIEnv* env, jobject obj, |
根据传入的地址,找到了之前新建的NativeMessageQueue
对象,调用它的pollOnce()
方法(注意参数中的timeoutMillis
):
1 | void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) { |
其实调用的是保存的NativeLooper
的pollOnce()
方法(注意参数中的timeoutMillis
)。
现在我将NativeLooper
中关于Native
事件循环的代码全部忽略,只分析与前面这个过程有关的部分:
1 | int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) { |
调用了pollInner()
方法(注意参数中的timeoutMillis
),分为两部分分析:
1 | int Looper::pollInner(int timeoutMillis) { |
第6行就是事情的关键,我们执行了系统epoll_wait()
调用(对我们之前创建的mEpollFd
epoll
实例),这是一个阻塞调用,当注册的Fd
有新内容或者到达超时时间时才会返回。我们还记得前面我们创建了mWakeEventFd
和eventItem
并把它注册到了mEpollFd
中。这样,只要mWakeEventFd
中有了新的内容,这行调用就会返回,解除阻塞。
现在我们可以推测,当有新消息到来时,正是以向mWakeEvendFd
中写入内容的方式来使nativePollOnce()
调用返回,达到了通知消息循环继续处理的目的。
如果没有新的消息呢?我们一步步传进来的timeoutMillis
就作为了epoll_wait()
的超时参数,一旦到达这个时间,epoll_wait()
函数就会返回,这达到了我们等待一段时间再去执行下一条消息的目的。
如果超时,20行检测出超时,跳转到Done
。
如果因fd
触发而返回,会进入28行的事件处理过程,这个过程依据拿到的eventItem
对象,检查fd
与events
标志,如果是我们之前设置的用于唤醒的mWakeEventFd
,调用awaken()
:
1 | void Looper::awoken() { |
做的事情非常简单,通过read()
读取并清零fd
中的数据。
你可能会想,为什么什么事情都没有做呢?因为这个mWakeEventFd
存在的唯一目的就是解除阻塞,现在这个目的已经达到了,我们只要重置它以便下一次使用就可以了。
Done
标号以后的代码与我们的过程无关,执行了自定义fd
消息处理相关的内容。最后将result
返回:
1 | return result; |
现在我们可以重新看待nativePollOnce()
函数,再次强调,它的作用是阻塞,当有新的消息或达到超时后返回。而这个核心的特性,完全是利用系统提供的epoll
机制实现的。
现在整个Java
消息循环的处理过程已经看完了,下面我们来结合常用的Handler
来讲解向消息队列中投入新的消息的过程。