Handler是面试必问系列问题之一。本系列将从初学者的视角分析面试中常见的问题。
handler源码学习(1) — Handler
handler源码学习(2) — Message
handler源码学习(3) — Looper
handler源码学习(4) — MessageQueue
本篇学习MessageQueue,主要解决以下几个问题
- 如何入队列
- 如何判断队列是否包含某个消息
- 如何移除消息
- 如何取消息
- 如何实现同步屏障
入队列
还记第一篇文章讲到无论是sendMessage还是postMessage最后都调用enqueueMessage方法,而enqueueMessage调用的就是MessageQueue的enqueueMessage(Message msg, long when)。源码如下
boolean enqueueMessage(Message msg, long when) {
//注释1.注意这里,如果msg.target==null,会直接抛出异常。后续讲同步屏障要用到
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
//注释2.如果正在使用,直接抛出异常
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}
//注意这里加锁了
synchronized (this) {
//如果正在放弃,直接return false,同时回收掉msg
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;
}
//注释3.标记为正在使用
msg.markInUse();
msg.when = when;
//mMessages就是队头消息
Message p = mMessages;
boolean needWake;
//如果队头消息为null,或者入队列消息触发时间为0(也就是立即触发消息),或者触发时间小于队头消息,也就是要入队列的消失触发时间最早。
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 {
// Inserted within the middle of the queue. Usually we don't have to wake
// up the event queue unless there is a barrier at the head of the queue
// and the message is the earliest asynchronous message in the queue.
//英文注释已经很清楚了,我直接翻译
//添加一个消息到队列中间,通常不需要唤醒事件队列,除非有屏障并且这个消息是最早的异步消息
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
//这里for循环就是遍历消息,拿到触发时间晚于入队列消息的消息和他的上一个消息
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;
}
// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
注释已经很清楚了。这里总结下:消息入队列会按触发时间进行入队列。这里留下三个疑问:
- 注释1处,msg.target == null会抛出异常,什么时候会msg.target == null?
- 目前看到的是在发送屏障消息时target为null
- 注释2处,msg.isInUse()会抛出异常,什么时候会msg.isInUse()?,什么时候会清除标记?
- 我目前看到的是在入队列的时候标记为正在使用,在obtain()的时候会被标记为不是正在使用。即使recycleUnchecked之后还是正在被使用状态。
- 哪些场景需要唤醒事件队列?
如何判断队列是否包含某个消息
其实这个重点是关注结论,我只给出一个方法的源码
boolean hasMessages(Handler h, int what, Object object) {
if (h == null) {
return false;
}
synchronized (this) {
Message p = mMessages;
while (p != null) {
if (p.target == h && p.what == what && (object == null || p.obj == object)) {
return true;
}
p = p.next;
}
return false;
}
}
可以看到对比的并不是对象,而是target,what和object相同。
从队列移除消息
void removeMessages(Handler h, int what, Object object) {
if (h == null) {
return;
}
synchronized (this) {
Message p = mMessages;
// Remove all messages at front.
//循环遍历,回收队头消息
while (p != null && p.target == h && p.what == what
&& (object == null || p.obj == object)) {
Message n = p.next;
mMessages = n;
p.recycleUnchecked();
p = n;
}
// Remove all messages after front.
//循环回收其他消息
while (p != null) {
Message n = p.next;
if (n != null) {
if (n.target == h && n.what == what
&& (object == null || n.obj == object)) {
Message nn = n.next;
n.recycleUnchecked();
p.next = nn;
continue;
}
}
p = n;
}
}
}
void removeMessages(Handler h, Runnable r, Object object) {
if (h == null || r == null) {
return;
}
synchronized (this) {
Message p = mMessages;
// Remove all messages at front.
while (p != null && p.target == h && p.callback == r
&& (object == null || p.obj == object)) {
Message n = p.next;
mMessages = n;
p.recycleUnchecked();
p = n;
}
// Remove all messages after front.
while (p != null) {
Message n = p.next;
if (n != null) {
if (n.target == h && n.callback == r
&& (object == null || n.obj == object)) {
Message nn = n.next;
n.recycleUnchecked();
p.next = nn;
continue;
}
}
p = n;
}
}
}
void removeCallbacksAndMessages(Handler h, Object object) {
if (h == null) {
return;
}
synchronized (this) {
Message p = mMessages;
// Remove all messages at front.
while (p != null && p.target == h
&& (object == null || p.obj == object)) {
Message n = p.next;
mMessages = n;
p.recycleUnchecked();
p = n;
}
// Remove all messages after front.
while (p != null) {
Message n = p.next;
if (n != null) {
if (n.target == h && (object == null || n.obj == object)) {
Message nn = n.next;
n.recycleUnchecked();
p.next = nn;
continue;
}
}
p = n;
}
}
}
private void removeAllMessagesLocked() {
Message p = mMessages;
while (p != null) {
Message n = p.next;
p.recycleUnchecked();
p = n;
}
mMessages = null;
}
private void removeAllFutureMessagesLocked() {
final long now = SystemClock.uptimeMillis();
Message p = mMessages;
if (p != null) {
if (p.when > now) {
removeAllMessagesLocked();
} else {
Message n;
for (;;) {
n = p.next;
if (n == null) {
return;
}
if (n.when > now) {
break;
}
p = n;
}
p.next = null;
do {
p = n;
n = p.next;
p.recycleUnchecked();
} while (n != null);
}
}
}
其实这块就是遍历调用调用recycleUnchecked()。唯一的考点是,为什么在移除消息的时候有些方法会先移除前面的消息,然后再移除后面的消息。答案是mMessages = n;为了拿到队头。其实一个while循环也可以。只是要讲会比较复杂。举个例子
消息队列 = {0,1,0,2}
第一个消息 = 0
此时移除消息0
两次while循环
先拿到 0符合,移除,再拿到1,不符合,队头就是1,然后执行第二个while循环,此时就不涉及到队头变化了
但如果是一个while循环
拿到0,符合,移除队头是1,又拿到1,不符合,又拿到0,又符合,此时就要判断是否要更换队头了
取消息
Message next() {
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
//唤醒轮循一次
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
//msg.target == null
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());
}
if (msg != null) {
if (now < msg.when) {
// Next message is not ready. Set a timeout to wake up when it is ready.
//下个消息还没到触发时间,记录下触发时间。最大值为Integer.MAX_VALUE
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;
}
} else {
// No more messages.
//如果消息为空,将下次唤醒时间设置为-1
nextPollTimeoutMillis = -1;
}
// Process the quit message now that all pending messages have been handled.
if (mQuitting) {
dispose();
return null;
}
//以下是关于idleHandler(闲时策略)相关的,后续分享。
······
}
······
}
}
以上便是取消息的源码。简单解释下流程。如果消息不为null且没有target,则开始屏障逻辑。还有我们之前提到的Looper.loop()不会阻塞,但是该方法会循环调用MessageQueue.next()。这个方法会阻塞,注意nativePollOnce(ptr, nextPollTimeoutMillis);这个方法,他的作用是在唤起一次,并记录下次触发的时间。如果没有消息,执行完一次之后,将会在这里挂起,有点类似Object.wait。那什么时候唤醒呢?其实在入队列的时候有这样一个方法nativeWake(mPtr),它的作用就是唤醒有点类似obejct.notify。经常面试被问到为什么Looper.loop()不会卡死,其实答案就在这里。因为没有消息时,会挂起。在有消息时,会在下次消息到达触发事件才触发。
同步屏障
private int postSyncBarrier(long when) {
// Enqueue a new sync barrier token.
// We don't need to wake the queue because the purpose of a barrier is to stall it.
synchronized (this) {
final int token = mNextBarrierToken++;
final Message msg = Message.obtain();
msg.markInUse();
msg.when = when;
msg.arg1 = token;
Message prev = null;
Message p = mMessages;
if (when != 0) {
while (p != null && p.when <= when) {
prev = p;
p = p.next;
}
}
if (prev != null) { // invariant: p == prev.next
msg.next = p;
prev.next = msg;
} else {
msg.next = p;
mMessages = msg;
}
return token;
}
}
其实很简单,就是Message.obtain()创建了一个msg,并且这个消息是没有target的,然后按触发时间插入消息队列。在next中,会优先处理这类消息。注意这个方法是private。要想使用只能用反射。
总结
- 如何入队列
- 按触发时间入队列
- 如何判断队列是否包含某个消息
- 并不是调用equals,而是比较message的what,when,object
- 如何移除消息
- 移除指定message时,先移除前面的,确定消息队列头,然后移除后面的。
- 如何取消息
- 不断的for循环取下一个消息,如果是屏障消息,则优先屏障消息。当没有消息时触发闲时策略,当没到下个消息触发时间时,会nativePollOnce,记录下下个触发的时间,阻塞。
- 如何实现同步屏障
- 通过postSyncBarrier发送屏障消息。该方法是私有方法需要通过反射调用。