【Android】Handler机制源码解析
文章目录
前言
Handler机制是一套在Android端用于跨线程传递消息的机制。它内部维护了一个由根据消息触发时间进行排列的消息队列。并由一个死循环不断的从消息队列中获取队列最前面的消息,然后交由Handler进行消息分发。
通常,在Android中切换线程时,会使用到Handler机制。比如,需要在子线程更新UI时,我们会调用一个在主线程实例化的Handler去发送一条通知消息,然后在回调处收到通知消息后,去更新UI。
那么,现在就有几个问题了:
- Handler是如何做到线程切换的?
- Handler中的延时消息是如何实现的?
- 如何保证消息是按照时间顺序到达?
- 所谓的同步消息屏障是什么?有什么用?
基础知识
在开始源码解析之前,需要先对几个Handler机制涉及到的类进行一点简单的了解。
整个机制包含以下4个主要的类:
- Handler,用于发送和处理消息。
- MessageQueue,用于对消息进行排列。
- Message,用于传递数据。内部是链式结构。
- Loop,用于读取消息队列并交由Handler分发。
OK,大概了解了每个类的作用之后,下面正式开始源码解析。
实例化Handler
在使用Handler机制时,我们都会先new一个Handler对象出来,并传入一个Callback或者重写Handler的handleMessage()
。
那么第一步,我们首先看看,当我们实例化Handler的时候,实际是干了哪些事情。
/**
* 通常使用的构造函数
*/
public Handler() {
this(null, false);
}
/**
* 实际最后调用的构造函数
*/
public Handler(@Nullable Callback callback, boolean async) {
...省略部分无关代码
// 检查是否调用过Looper.prepare()
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread " + Thread.currentThread()
+ " that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
构造函数中,首先调用Looper.myLooper()
获取到一个Looper对象,然后再获取到Looper对象中的消息队列mQueue
。另外的mCallback
在默认情况下是传入的null,当然我们也可以传入我们自己的Callback。async
这个属性在后面会用到,主要用来决定发送的消息是否是异步消息。
在这里,我们看到了一个熟悉的报错信息:Can't create handler insode ....
。根据源码可以知道,这是因为Looper.myLooper()
返回null导致的。提示我们,需要先调用Looper.prepare()
才行。那么先看看Looper.prepare()
干了些什么。
public static void prepare() {
prepare(true);
}
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
// 向当前线程的map中放入Looper
sThreadLocal.set(new Looper(quitAllowed));
}
首先调用sThreadLocal.get()
,判断是否为null。根据错误信息,可以猜到sThreadLocal.get()
返回的就是一个Looper对象。
然后,向sThreadLocal
中放入了一个刚刚实例化的Looper。
sThreadLocal
这个变量主要是保存一些当前线程的数据。所以这里实际上就是为我们实例化一个Looper并保存到当前线程。
到这里,其实不难猜测,Looper.myLooper()
实际上就是调用了sThreadLocal.get()
去获取一个当前线程的Looper对象。
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
果不其然。
到这里,实例化Handler的源码基本就解析完了。我们来总结一下,当我们实例化一个Handler对象时,实际上做了哪些操作:
- 调用
Looper.myLooper()
获取一个当前线程的Looper,如果没有的话会报错提示先调用Looper.prepare()
去新建一个Looper并保存到当前线程。 - 获取Looper中的消息队列
mQueue
。
当实例化完Handler之后,下一步就是构造消息了。
构造消息Message
可能很多人都听过,当我们要创建一条消息时,应该使用Message.obtain()
而不是直接new Message()
,那么这是为什么呢?
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
通过源码可以知道,obtain()
实际上会进行一个复用的判断,如果能复用的话就复用,而不是直接新建。这个复用机制可以减少新对象的创建,优化内存使用。
我们知道,Android的屏幕刷新时16ms一次的,而屏幕刷新就是使用的Handler机制实现的。那么假如不进行Message复用的话,意味着每16ms就要创建一个Message对象。
知道如何创建Message之后,我们再来看看,Message内部有哪些比较关键的东西。
asynchronous:标记该消息是否为异步消息。
callback:当该消息被分发的时候会执行的动作。
data:该消息携带的数据。
inUse:标记该消息是否正在被使用。
target:将会由哪个Handler处理该消息。
whta:标记该消息。
when:该消息何时被分发
总结一下:
构造消息时,尽量使用Message.obtain()
,而不是去new Message()
,这样可以减少新对象的创建。
一个Message,必不可少的就是target
,因为它决定了这条消息是由谁处理的。
发送消息Handler.sendMessage()
消息构造好之后,就是将消息发送出去了。此时我们只需要调用Handler.sendMessage()
就可以了,然后就可以在Handler.handleMessage()
或者Callback中去接收发送的消息了。这样就完成了线程之间的切换。那么这一套操作的内部逻辑是怎么样子的呢?
public final boolean sendMessage(@NonNull Message msg) {
return sendMessageDelayed(msg, 0);
}
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
可以看到,当我们调用sendMessage()
的时候,实际上还是调用到了enqueueMessage()
。根据方法名可以猜测,这个方法应该是将Message放入到消息队列中去了。
/**
* 将消息Message入队MessageQueue
* @param queue
* @param msg
* @param uptimeMillis 该消息触发的时间,系统自启动运行的时间+传递消息时设置的延时时间
* @return
*/
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;// 这里重刷了一次msg的target对象
msg.workSourceUid = ThreadLocalWorkSource.getUid();
if (mAsynchronous) {// 是否异步消息,默认的使用是false
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
这里需要注意一下,message.target
在这里被设置成了当前的Handler。也就意味着假如在当初构造消息的时候,我们设置了target,那么其实是无效的。Message由哪个Handler发送,那么它也就被哪个Handler处理。
这里设置完target之后,会根据当初构造Handler的时候传入的参数判断是否要将消息设置为异步消息。
然后调用消息队列queue
的enqueueMessage()
方法。所以实际上,消息入队的操作还是在MessageQueue
中进行的,在Handler中仅仅只是对Message进行一些改变而已。
由于MessageQueue.enqueueMessage()
代码过长,这里我将一段一段的进行分析,就不一次性贴全部的代码了。
首先是消息的完整性检查:
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}
target
一般来说不会出现为null的情况,因为我们用Handler.sendMessage()
的时候,会重新设置一次target。主要是第二个检测,检测消息是否正在已经被使用过了。
然后,是使用了一个同步代码块(synchronized)去进行正在的消息入队操作。
synchronized (this) {
if (mQuitting) {
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
Log.w(TAG, e.getMessage(), e);
msg.recycle();// 回收消息
return false;
}
...省略部分代码
}
判断当前消息队列是否正在退出,根据源码跟进发现,mQuitting
下面这个调用链下会变为true:
MessageQueue.quit()
---------Looper.quit()
即当Looper退出后,消息队列也将标记为退出状态。
做完上述的检测性动作后,下面将正在的开始进行消息入队的操作,整个入队操作都是在synchronized
的修饰之下的。
msg.markInUse();
msg.when = when;
Message p = mMessages;
boolean needWake; // 标记是否需要唤醒事件队列
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
// 新的头部,如果事件队列被阻塞,那么唤醒它
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
// 非头部消息
...省略部分代码
}
首先将消息标记为已使用的状态,再把消息的触发时间给赋值。如果你有仔细看之前的代码,可以知道,消息的触发时间=系统开机以来的时间+消息延迟的时间。
注意这里使用到了一个变量mMessages
,这个变量表示当前消息队列中,最前面即头部的那条消息。在最开始的基础知识部分我提到过,Message的内部是使用的链式结构,那么对于消息队列而言,只需要知道头部是谁即可。
另外还有个布尔变量needWake
,它用于决定,是否要唤醒该消息队列。关于消息队列的阻塞和唤醒,在后面获取消息的时候再仔细讲,这里只分析什么情况下,将会进行唤醒。
现在我们看到if语句,判断了3个条件满足一个即可:
- 当前消息队列没有头部消息,即消息队列为空。
- 将要入队的消息,触发时间为0,即立刻触发。
- 将要入队的消息触发时间小于当前的头部消息的触发时间。
三选一,只要满足一个,那么该消息将被插入到消息队列的头部中,那么之前的头部则将作为该条消息的next
。这里的needWake
的值由mBlock
觉得,即如果消息队列阻塞中,那么将唤醒消息队列。
现在来看else的代码块。根据前面的条件可以知道,如果走到else代码块,那么意味着这条入队的消息,触发时间是大于当前的头部消息的。那么我们就需要从头部消息开始向下寻找,找到第一个触发时间大于当前消息的。
// 不是头部消息,那么需要遍历去找到触发时间最相近的一条消息
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
// 要么一直找到末尾都没
// 要么找到最近的一个触发事件大于当前消息的位置
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
由于我们不知道这条链有多长,所以使用的是一个死循环去遍历。
首先看到第一行代码,是用于决定是否要唤醒消息队列的,需要同时满足以下三个条件:
- 当前消息队列已被阻塞。
- 当前头部消息的target为null。
- 将要入队的消息是异步消息。
这里你可能有疑问了,为什么头部消息的target可以为null呢?明明在enqueueMessage()
这个方法的一开始就判断了message.target
是否为null啊?这里涉及到一个名词,叫:同步消息屏障。这里将不展开,你只需要知道,只要消息的target
为null,那么它就会被认为是一条屏障消息,即所谓的同步消息屏障。
继续看我们的后续代码。
逻辑很简单,判断下一条消息是否为null或者触发时间是否大于当前消息的触发时间,如果满足一个,那么就将当前消息插入到这条消息的前面。
这里又出现一个有意思的地方了:
if (needWake && p.isAsynchronous()) {
needWake = false;
}
为什么在前面明明已经满足唤醒队列的情况下,如果发现消息队列中某条消息是异步消息,又不唤醒消息队列了呢?这个问题,留到后面的轮询消息的时候我将会解密。
到这里,我们的发送消息流程就结束了。其实,虽然对于调用者而言,调用的是sendMessage()
,实际上内部叫做enqueueMessage()
,整个流程叫消息入队更贴切。
现在我们总结一下:
当调用Handler.sendMessage()
的时候,实际上最终是调用到MessageQueue.enqueueMessage()
。所谓的发送消息,其实只是将消息加入到了消息队列中去等待被分发而已。
另外,我们通过分析入队的代码,可以知道,整个Handler机制是如何保证消息按触发时间进行消息分发的了。因为在消息入队的时候,会将消息按照触发时间的顺序进行插入,这样,在后续获取消息的时候,只需要从消息队列的头部,一条接一条的获取即可。这就解答了在本文最开头问道的问题:
- Handler中的延时消息是如何实现的?
- 如何保证消息是按照时间顺序到达?
- 同步消息屏障是什么?就是
message.target
为null的消息。
但是还有一个问题没有解决,那就是到底是如何做到线程切换的呢?这个问题将在下面的,获取消息中进行解答。
获取消息MessageQueue.next()
首先你可能有个疑惑,在使用Handler的时候,我明明只需要发送消息就可以了啊,不用去调用MessageQueue.next()
去获取消息啊?的确,因为这个动作是机制内部完成的。那么这里就有个问题,到底是谁在帮我们完成获取消息这个动作呢?没错,就是Looper这个类。
还记得,如果我们在子线程中去实例化一个Handler的时候,我们需要怎么做吗?
Looper.prepare();
handler = new Handler();
Looper.loop();
Looper.prepare()
我们已经看了,就是新建一个Looper并保存到当前线程中。
那么Looper.loop()
是干什么的呢?可能很多人都遇到过,在子线程中使用Handler发送消息明明没有报错,一切正常,但是就是收不到消息。这个时候,你去百度一下,很多人都会让你加上这么一句话:Looper.loop()
。神奇的事情发生了,消息能正常的获取到了。
因此我们不难判断,Looper.loop()
就是帮助我们获取消息的地方。
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
...省略部分代码
for (;;) {
// 从消息队列中获取消息
Message msg = queue.next(); // might block 可能阻塞
if (msg == null) {
// No message indicates that the message queue is quitting.
// 翻译:没有消息表示消息队列正在退出。
return;
}
...
try {
// 开始分发消息
msg.target.dispatchMessage(msg);
...
} catch (Exception exception) {
...
throw exception;
} finally {
...
}
...
msg.recycleUnchecked();
}
}
整个方法,我删除了一些非功能性的代码,只留下主要的获取消息以及分发消息的代码。
那么整个逻辑就很简单了,死循环一直调用MessageQueue.next()
获取消息,如果返回null的话表示消息队列正在退出,那么Looper也将退出工作,不再循环,否则将消息调用message.target.dispatchMessage()
传递给目标Handler处理。
你可能有个疑问,似乎没有看到延时的代码,那么这样一直死循环的去读取消息队列,不会造成卡顿吗?莫急莫急,马上你就知道了。我们来看看MessageQueue.next()
。
由于源码太长,因此我将一段段的贴代码并进行讲解:
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
关于mPtr
这个变量,是底层的native方法中的一个指针,由于我不会C,所以我也不太清楚这个指针什么情况下将会为0。所以这段代码我们眼熟一下就好,关键还是后面的。
int nextPollTimeoutMillis = 0;
for (;;) {
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// 找到下一条需要分发的消息
}
}
又见for死循环,为什么我这里会省略掉寻找下一条消息的代码呢?因为我想提一提这个nativePollOnce()
底层函数。它会根据第二个参数nextPollTimeoutMillis
去进行阻塞,有以下2种情况:
- -1表示一直阻塞,直到被唤醒。
- 大于等于0表示阻塞
nextPollTimeoutMillis
这么久。
我找了很久,在底层源码中找到了下面的代码,位于/system/core/include/utils/Condition.h:
inline status_t Condition::wait(Mutex& mutex) {
return -pthread_cond_wait(&mCond, &mutex.mMutex);
}
inline status_t Condition::waitRelative(Mutex& mutex, nsecs_t reltime) {
struct timespec ts;
...省略部分代码
return -pthread_cond_timedwait(&mCond, &mutex.mMutex, &ts);
}
主要关注这两个方法pthread_cond_wait()
和pthread_cond_timedwait()
,这两个方法是Linux系统的方法。查阅资料知道他们的作用是这样的:
pthread_cond_wait | 线程等待信号触发,如果没有信号触发,无限期等待下去。 |
---|---|
pthread_cond_timedwait | 线程等待一定的时间,如果超时或有信号触发,线程唤醒。 |
这里我提供一下整个native层的调用顺序。
- /frameworks/base/core/jni/android_os_MessageQueue.cpp:188
- /frameworks/base/core/jni/android_os_MessageQueue.cpp:107
- /frameworks/base/native/android/looper.cpp:52
- /frameworks/hardware/interfaces/sensorservice/libsensorndkbridge/ALooper.cpp:43
- /system/core/include/utils/Condition.h:117
讲完了这个方法,现在你应该知道为什么在Looper.loop()
中死循环还不用延时都不会卡顿的原因了吧?就是因为消息队列会进行阻塞,这样Looper中也就一同被阻塞住了。
现在,假设消息队列被唤醒了,我们要去看看现在的消息队列中是否有满足触发条件的消息去分发还是继续阻塞。
// Try to retrieve the next message. Return if found.
// 翻译:尝试检索下一条消息。 如果找到则返回。
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;// 下一条消息
Message msg = mMessages;// 队列头消息
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
// 翻译:被障碍挡住了。 在队列中查找下一个异步消息。
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
...省略部分代码
首先进来一个判断,当前的头部消息不为null且头部消息的target为null。还记得我前面说的吗?target为null的消息就是屏障消息,这里,我们就可以看到所谓的屏障消息到底有何作用了:如果检测到屏障消息,那么下一条消息将不再按照正常的顺序,而是优先向下寻找最近的一条异步消息。
所谓的异步消息,从代码看上来,其实只是优先级比普通消息高而已。因为我们看到,如果在出现屏障的情况下,所有同步消息都会被阻挡住,只有异步消息能正常进行分发。
跳过这一段,我们看看正常情况下的消息是如何寻找并返回给Looper的。
if (msg != null) {
...有消息
} else {
// 无消息
// No more messages.
nextPollTimeoutMillis = -1;
}
// Process the quit message now that all pending messages have been handled.
// 翻译:现在已处理所有挂起的消息,请处理退出消息。
if (mQuitting) {
dispose();
return null;
}
if (pendingIdleHandlerCount <= 0) {
// No idle handlers to run. Loop and wait some more.
// 翻译:没有空闲的处理程序可以运行。 循环并等待更多。
mBlocked = true;
continue;
}
// We only ever reach this code block during the first iteration.
// 翻译:我们只会在第一次迭代时到达此代码块。
...省略部分代码
// Reset the idle handler count to 0 so we do not run them again.
// 翻译:将空闲处理程序计数重置为0,这样我们就不再运行它们。
pendingIdleHandlerCount = 0;
...省略部分代码
代码并不多,并且官方还提供了注释。首先是检测是否存在头部消息,如果头部消息都没有的话,那就意味着整个消息队列目前是没有任何消息的,此时将nextPollTimeoutMillis
设置为-1,然后走到下面的判断,根据注释我们可以知道将会继续循环。
如果有消息:
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
// 翻译:下一条消息尚未准备好。 设置超时以在就绪时唤醒。
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;// 不再阻塞
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;// 结束死循环并将消息返回给Looper
}
判断当前时间是否小于消息触发时间,如果小于,则将nextPollTimeoutMillis
设置为触发事件。需要注意的是,这里表明了,nextPollTimeoutMillis
的最大值为Integer.MAX_VALUE
。
如果当前事件大于等于消息触发时间,那么就将该消息从队列中移除,并将改消息返回给Looper。
到这里,获取消息的源码解析完毕。
我们来总结一下:Looper.loop()
使用一个死循环不断的调用MessageQueue.next()
获取消息,而在MessageQueue.next()
中,会调用底层方法去阻塞当前消息队列,直到返回消息或者消息队列退出。然后Looper.loop()
将调用Message.target.dispatchMessage()
将消息传递给Handler去处理。
而且我们需要注意,在Looper.loop()
时,是运行在调用Looper.prepare()
的这个线程的,也就意味着Message.target.dispatchMessage()
也运行在同一个线程。也就是我们实例化Handler的线程。注意到了吗?不知不觉中,就完成了线程的切换。Handler.sendMessage()
可以在任何线程调用,最终都消息都会去到实例化Handler时的线程。
下一步,我们看看最后的消息是如何回调到我们Callback.handleMessage()
或者Handler.handleMessage()
的。
处理消息Handler.dispatchMessage()
当Looper调用Message.target.dispatchMessage()
时,就将消息传递到了我们的Handler这里。
/**
* Handle system messages here.
*/
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
// 有callbacl就用callback处理消息,没有则用当前handler处理
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
// 默认为空实现,需要自行重写
handleMessage(msg);
}
}
private static void handleCallback(Message message) {
message.callback.run();
}
非常简单的几行代码,如果消息有本身设置的callback
,那么就调用callback.run()
。
如果消息没有设置callback,那么将根据是否有为Handler设置callback进行消息的分发。有Handler.callback则调用callback.handleMessage()
,否则调用Handler本身的handleMessage()
。
总结
到此,我们已经基本把Handler这套机制给大体上理清楚了。现在我们来回顾最开始提到的几个问题:
- Handler是如何做到线程切换的?
答:使用到了Looper,而Looper是根据线程进行保存的,并且分发消息也是由Looper完成的。
- Handler中的延时消息是如何实现的?
答:在消息队列中,会在底层阻塞消息队列获取下一条消息,直到到达设定的时间点。
- 如何保证消息是按照时间顺序到达?
答:在每条消息入队的时候,会找到最近的一条触发时间大于该消息的触发时间的消息,然后将要入队的消息插入到这条消息的前面。这样,就保证整个消息队列是按照触发时间,从小到大进行排序的。
- 所谓的同步消息屏障是什么?有什么用?
答:当Message.target
为null的时候,就是同步消息屏障,也叫屏障消息。当获取消息的时候,遇到屏障消息时,将不再按照正常的逻辑去向下获取消息队列,而是只获取异步消息。这样,相当于提高了异步消息的处理优先级。
上一篇: DIV 切换(二)
下一篇: Android Handler源码解析