背景知识
进程与线程
类似其他操作系统的相应概念,Android 的进程拥有独立的虚拟内存空间,而线程与同一进程内的其他线程共享内存,只拥有各自的栈空间、程序计数器和寄存器等少量独占资源。
当 Android 启动一个之前未运行的应用的组件时,会为其启动一个包含单个主线程"main"的新的 Linux 进程。默认情况下,同一应用的所有组件会在此进程内启动,并且使用同一执行线程。
开发者可以人为安排组件在单独的进程中运行,并且为进程创建额外的线程。
Android 线程
每个 Android 应用都有一个主线程,也称界面线程,负责绘制 UI,处理用户交互以及接收生命周期事件。Android UI 操作不是线程安全的,这些操作必须在主线程执行。
为了不拖慢主线程,任何长时间运行的计算和操作都应在后台线程完成,避免 ANR。使用多线程还可以充分利用多核处理器的优势,通过并行提高运行速度。
- 创建线程的方法
- Java 的线程方法
- 重载 Thread.run() 方法
- 实现 Runnable 类
- Android 的线程方法
- AsyncTask
- IntentService
- HandlerThread
- Executors 中创建线程池 (
newFixedThreadPool
,newScheduledThreadPool
,newWorkStealingPool
)
- Java 的线程方法
从其他线程访问主线程的方法
Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable, long)
- 使用 Handler
Handler 介绍
Handler
是 Android 系统线程管理框架的一部分,使用消息队列实现线程之间的通信。
Handler
允许你发送和处理关联到线程的 MessageQueue
的 Message
和 Runnable
对象。新的 Handler
会绑定到创建者线程的消息队列上。
消息驱动机制的四要素:
- 接收消息的“消息队列”:
MessageQueue
- 阻塞式地从消息队列中接收消息并进行处理的“线程”:
Thread
&Looper
- 可发送的“消息的格式”:
Message
- “消息发送函数”:
Handler.post(Runnable)
&Handler.sendMessage(Message)
Handler 的两大主要作用:
- 安排
Message
和Runnable
在未来某个时刻运行。 - 安排在其他线程中执行的动作。
使用 Handler:
-
接收者线程:
- 如果是子线程,判断其是否已有
Looper
,如果没有,运行Looper.prepare()
- 实例化 Handler 对象
mHandler
,实现handleMessage
方法(重载方法或实现Callback
接口)。 - 如果接收者是子线程,调用
Looper.loop()
方法,使得 Looper 调用handleMessage
方法对消息进行处理;如果是主线程,启动时已经调用了Looper.prepare()
Looper.loop()
方法。 - 如果子线程不需要再处理消息,调用
Looper.myLooper().quit()
退出消息轮询
- 如果是子线程,判断其是否已有
-
发送者线程:
- 创建 Message 对象,设置 Message 的参数
- 使用
mHandler.sendMessage(Message)
方法将 Message 传入 Handler 的消息队列
Handler 常用方法:post(Runnable)
, postAtTime(Runnable, long)
, postDelayed(Runnable, Object, long)
, sendEmptyMessage(int)
, sendMessage(Message)
, sendMessageAtTime(Message, long)
, sendMessageDelayed(Message, long)
Looper 介绍
Looper 负责处理 MessageQueue 中的 Message。
一个线程内最多只有一个 Looper 对象,否则会抛出异常。
使用 Looper:
1. 判定是否已有 Looper
并 Looper.prepare()
2. 做一些准备工作
3. 调用Looper.loop()
,线程进入阻塞态
Handler 源码分析
Handler
frameworks/base/core/java/android/os/Handler.java
重要成员变量:
1 |
|
构造方法
1 | public Handler(boolean async) { Callback callback, |
第一种构造方法首先获取当前线程的 Looper。如果当前线程没有初始化 Looper,则会抛出异常。第二种构造方法直接传入要使用的 Looper 作为参数。之后初始化 mCallback
和 mAsynchronous
成员。
其他方法
post
系列方法会调用 getPostMessage
方法将 Runnable 包装为 Message,然后使用 sendMessage
系列方法发送。
sendMessage
系列方法最终会调用 enqueueMessage
私有方法,最后调用 MessageQueue.enqueueMessage
方法加入消息队列。
removeMessages
和 removeCallbacks
系列方法会调用 MessageQueue.removeMessages
方法从消息队列中移除指定消息。
obtainMessage
系列方法会调用 Message.obtian
系列方法,从消息池中获取一个 Message
对象。
1 | private static Message getPostMessage(Runnable r) { |
getPostMessage
方法将 Runnable 对象赋值给 Message 对象的 callback
成员变量,从而将其包装为 Message。
1 | public void dispatchMessage( { Message msg) |
dispatchMessage
方法处理消息的方式有三种:
- 先检查 Message 对象的
callback
是否非空,若是,表明这个 Message 是使用post
方法发送的,运行该 Runnable。 - 否则,检查是否初始化了
mCallback
成员变量,若是,运行其中的handleMessage
方法。mCallback 成员变量可以在调用 Handler 构造方法时传入 Callback 接口的实现来初始化。 - 如果没有实现 Callback 接口,或上一步方法返回值为
true
,则运行 Handler 本身的handleMessage
方法。
getMain
方法会返回静态成员变量 MAIN_THREAD_HANDLER
的值。如果该变量没有初始化,就创建一个使用主线程 Looper 对象初始化的 Handler 对象,对该变量赋值并返回。
createAsync
系列方法调用 Handler 对应构造方法,其中async=true
runWithScissors
TODO
getIMessenger
TODO
Looper
frameworks/base/core/java/android/os/Looper.java
1 | public final class Looper { |
构造方法
1 | private Looper(boolean quitAllowed) { |
构造方法是私有方法,只能通过 Looper.prepare()
来初始化 Looper
。
接下来就看看Looper.prepare()
:
1 | public static void prepare() { |
首先尝试获取静态线程局部变量 sThreadLocal
,如果不为空,就说明已经创建过 Looper
对象,抛出RuntimeException
;否则,就将 sThreadLocal
的值设置为新创建的 Looper
对象。
同一线程内的所有对象共享同一个静态变量 sThreadLocal
,因此能保证一个线程至多只有一个 Looper
对象。
启动主线程相关代码:
frameworks/base/core/java/android/app/ActiviytThead.java
1 | public static void main(String[] args) { |
1 | public static void prepareMainLooper() { |
主线程启动时调用 PrepareMainLooper()
方法。这个方法会调用 prepare(quitAllowed)
方法,其中 quitAllow=False
,之后将 sMainLooper
赋值为自身的 Looper。其他线程可以使用 Looper.getMainLooper()
方法来访问主线程的 Looper。
实例化 Looper 之后,创建 ActivityThread 实例,将线程注册到系统服务,最后调用 Looper.loop()
进入消息处理循环。
至此,应用程序的启动过程就完成了。正常情况下主线程会一直处于消息循环中,这样应用程序组件就可以利用消息处理机制来实现业务逻辑。
消息循环
看看Looper.loop()
怎样进行消息处理循环:
1 | public static void loop() { |
loop()
方法会进入一个死循环,不断从 MessageQueue 取出消息,交给 Handler 处理。
如果消息队列为空,queue.next()
方法会阻塞,直到有消息进来,再取出消息返回。除非调用quit()
或 quitSafely()
方法结束轮询,queue.next()
才会返回null
,结束循环。
MessageQueue
frameworks/base/core/java/android/os/MessageQueue.java
frameworks/base/core/jni/android_os_MessageQueue.cpp
构造方法
1 | MessageQueue(boolean quitAllowed) { |
可以看出主要工作在 nativeInit()
函数进行,这是一个 JNI 方法,在讨论线程阻塞与唤醒时再回到这里。
插入与移除消息
消息队列以链表的方式储存,MessageQueue 的 mMessages
成员变量保存链表的第一个消息。
enqueueMessage
方法插入消息到队列,removeMessages
方法移除消息。插入和移除消息时会保证消息队列总是按 when
属性递增的顺序排列,也就是链表的头总是最紧急要处理的消息。
1 | boolean enqueueMessage(Message msg, long when) { |
mBlocked = true
时,以下几种情况下,插入消息会设置 needWake = true
,唤醒接收者线程:
- 消息队列为空(
p == null
) - 消息设置为立即处理(
when == 0
) - 消息预定的时刻早于队列头部的消息(
when < p.when
) - 消息队列被同步屏障暂停,而插入的消息是最早一条要处理的异步消息
如果需要唤醒,会调用 nativeWake(mPtr)
方法唤醒线程。我们待会讨论这个方法。
插入与移除同步屏障
postSyncBarrier
方法会插入一条同步屏障(Sync Barrier)消息到消息队列。next
方法读取到同步屏障消息后,会停止处理同步消息,只处理异步消息。如果不设置生效时间或设置为 0,屏障将立即生效。该方法返回一个 token,用于调用 removeSyncBarrier
方法移除该同步屏障。
removeSyncBarrier
方法会移除插入的同步屏障消息,使消息队列继续处理同步消息。
取下一条消息
MessageQueue.next()
方法将下一条待处理的消息返回给 Looper。其内部实现了阻塞线程、同步屏障、定时处理消息、处理空闲情况等机制。没有消息时,该方法会阻塞线程,直到新的消息到达,或者定时器到期。
1 | Message next() { |
首先获取 NativeMessageQueue 对象的指针。如果该指针为空,说明 Native 对象没有正确初始化,返回 null 结束消息循环。
接下来进入取待处理消息的循环。
若下次查询的超时时间不为0,Binder.flushPendingCommands();(?)
调用 JNI 方法 nativePollOnce
,这个方法就是没有消息时阻塞的源头,先放在一边。
接着看下面的过程,仍在 for 循环内:
1 | synchronized (this) { |
mMessages
成员变量作为链表头,是这一轮循环搜索消息的起始点,临时变量msg
保存要取出的消息。
-
第 6 到 12 行,如果
msg.target
为空,说明这是一个由postSyncBarrier
方法设置的同步屏障(Sync Barrier)消息,那么快进msg
到下一个异步消息。 -
接下来,如果
msg
不为空,首先判断消息的唤醒时间是否已到,没有到就设置下次唤醒时间为该唤醒时间。否则,从消息队列链表中取出msg
:
- 如果之前有快进过(
prevMsg != null
),prevMsg
指向下一个消息。 - 否则,
mMessages
向前移动。
最后返回当前消息 msg
。
如果上一步判断当前消息为空,设置 nextPollTimeoutMillis = -1
, 执行nativePollOnce
方法时将一直阻塞,直到有新的消息到达。
-
36 到 39 行,如果
mQuitting
变量设置为true
, 现在就是处理退出请求的时候:扔掉剩余请求,并返回 null。 -
44 行开始,就是处理 IdleHandler 的代码。如果第一次遇到空闲(没有消息要处理)的情况,初始化
pendingIdleHandlerCount
计数。 -
接下来,如果没有 IdleHandler 需要运行,设置
mBlocked = true
,这会通知enqueueMessage
方法在加入消息时唤醒当前线程。
这一节的最后,如果之前没有初始化mPendingIdleHandlers
,执行初始化。
离开临界区,继续处理 IdleHandlerr:
1 | for (int i = 0; i < pendingIdleHandlerCount; i++) { |
遍历要处理的 IdleHandler,执行其 queueIdle()
方法。如果该方法返回 false
,表明整个消息循环中该方法只需执行一次,就从 mIdleHandlers
中移除对应的 IdleHandler。
最后重设 pendingIdleHandlerCount
计数为 0,这样下个循环就不会再次处理 IdleHandler 相关逻辑。
总结一下整个方法的行为:
线程阻塞与唤醒
初始化 NativeMessageQueue
前面 MessageQueue 初始化时,调用 nativeInit()
初始化其 C++ 对象:
1 | static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) { |
新建了 NativeMessageQueue 对象并增加其强引用计数,返回对象指针。
1 | NativeMessageQueue::NativeMessageQueue() : |
构造方法尝试获取当前线程的 C++ 层 Looper 对象,如果没有获取到,则新建一个并绑定到当前线程。
来看看这个 C++ 层的 Looper:
system/core/libutils/include/utils/Looper.h
、
system/core/libutils/Looper.cpp
1 | Looper::Looper(bool allowNonCallbacks) |
构造方法使用 eventFd
系统调用获取了 mWakeEventFd
,作为后续 epoll
用于唤醒的文件描述符。
有了 Fd,再进入 rebuildEpollLocked()
调用:
1 | void Looper::rebuildEpollLocked() { |
在第 6 行,通过系统调用 epoll_create1
初始化一个 epoll 实例,之后创建 epoll_event 结构 eventItem
并设置 events
属性,将 fd
设置为之前创建的 mWakeEventFd
。
在第 13 行,通过系统调用 epoll_ctl
将 eventItem
注册到 epoll。
epoll 允许我们对多个文件描述符进行监听。注册监听的 fd
之后,调用 epoll_wait
函数,当 fd
指向的对象数据可用时,epoll_wait
函数就会返回,同时从传入的events
指针返回发生改变的 fd
对应的 eventItem
。
阻塞
前面说过如果消息队列没有消息,线程就会被阻塞。阻塞的调用路径是 Looper.loop() -> MessageQueue.next() -> MessageQueue.nativePollOnce(long , int)
。我们从 nativePollOnce
开始继续追踪。
JNI 方法 nativePollOnce
调用 NativeMessageQueue::pollOnce
方法,进一步调用 Looper::pollOnce
方法。
1 | int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) { |
最后来到 pollInner
:
1 | int Looper::pollInner(int timeoutMillis) { |
第4行就是关键所在。通过执行系统调用 epoll_wait
,线程将会阻塞,直到注册的 fd 有新数据或者到达超时时间才会返回。前面 MessageQueue 初始化时注册了 mWakeEventFd
,当它有新数据时 epoll_wait
就会返回,解除阻塞。由此推测,唤醒线程的方法 nativeWake
正是通过向 mWakeEventFd
写数据的操作,来解除阻塞,实现其功能的。这里写入的数据并不重要,只是利用 epoll
机制提供的阻塞和唤醒功能。
解除阻塞后,接下来进入事件处理的过程。遍历返回的 eventItems
,检查是否有 fd 与 mWakeEventFd
相同。如果有,执行 awoken
方法。该方法不断读取 mWakeEventFd
以清空其内容,便于下次使用。
这里省略了大量与处理 Native 层消息相关的代码,因为这与本次主题无关。
唤醒
nativeWake
用于唤醒功能,调用 NativeMessageQueue::wake()
,进一步调用 Looper::wake()
。正如之前预测的,该方法向 mWakeEventFd
写入数据,实现其唤醒的功能。
调用链
Q&A
Q: Looper 如何保证线程唯一的?
A: Looper 的构造方法是私有的,只能通过 Looper.prepare()
创建,通过 Looper.myLooper()
获取。
Looper.prepare()
会检查静态线程局部变量 sThreadLocal
的值是否已设定,只有未设定时才会将其设为新建的Looper 对象,否则抛出异常。同一线程内的所有对象共享同一个静态变量sThreadLocal
,因此能保证一个线程至多只有一个 Looper 对象。
Q: ThreadLocal如何实现数据隔离的?与加锁实现的区别是什么?
A: 每个 Thread 对象中保存一个 ThreadLocalMap 对象,由 ThreadLocal 中的方法操纵。这个 Map 的 key 是 ThreadLocal 的弱引用,value 就是储存的对象。
当调用 ThreadLocal.get()
方法时,先判断该 ThreadLocalMap 是否非空,再使用 ThreadLocal 对象作为 key 查询并返回储存的对象。查询用的 hash code 是在ThreadLocal 对象初始化时调用 threadLocalHashCode
生成的。若 ThreadLocalMap 为null,或没有与 key 对应的对象,则调用 setInitialValue()
设初值并返回该值。
ThreadLocal 和 synchonized 都用于解决多线程并发访问,但是 ThreadLocal 与 synchronized 有本质的区别。
- 机制不同
- synchronized 是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。
- ThreadLocal 为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象。
- 数据共享状态不同
- synchronized 仅提供一份数据,使得多个线程间通信时能够获得数据共享。
- ThreadLocal 为每个线程提供一份数据,隔离了多个线程对数据的数据共享。
Q: 想在消息队列阻塞前处理一个事件应该如何实现?
A: 调用 next
方法取下一条消息时,消息队列会在当前消息为空时阻塞,以等待新的消息。通过 addIdleHandler
方法,添加处理空闲状态的代码,可以在下次循环到 nativePollOnce
阻塞之前执行期望的操作。
Q: 主线程与其他线程的不同之处有哪些?
A: 主线程在应用启动时由 Android 系统启动,子线程由应用开发者主动开启。主线程会在启动时调用 Looper.prepareMainLooper()
和 Looper.loop()
方法,进入消息循环。主线程进入 Looper.loop()
方法后不会退出消息循环,子线程可以通过 Looper.quit()
退出消息循环。主线程负责处理 UI 事件和 Broadcast 消息,若超时未响应会触发 ANR。
Q: Message
和 Runnable
在 Handler 中什么区别?
A: 逻辑是一样的,post
方法中的 Runnable
会被封装成 Message
,再用 SendMessage
方法发送。
Q: Handler 如何实现消息定时处理?
A: 这一功能是在 MessageQueue.next()
方法实现的。MessageQueue 取出消息返回前,会对比该消息的 when
属性与当前时间。如果还没有到设定的时刻,就会设置nextPollTimeoutMillis
变量,使得线程在给定的时刻唤醒。否则,就会返回该消息到 Looper,Looper 再调用 dispatchMessage
交给 Handler 处理。
参考文献
- Android SDK, android.os.Handler
- Android Developers Documentation, https://developer.android.com/reference/android/os/Handler
- Android 源码分析 --Handler 机制的实现与工作原理,https://juejin.im/post/5910522f1b69e6006858b830
- Android 消息机制(一)消息队列的创建与循环的开始 Looper与MessageQueue, https://www.viseator.com/2017/10/22/android_event_1/
- Android Handler的使用方式和注意事项, https://juejin.im/post/5910533dac502e006cfe01cd
- 理解 Java 中的 ThreadLocal, https://droidyue.com/blog/2016/03/13/learning-threadlocal-in-java/