一位热心群友在面试时抛了一个问题:
说下 handler 机制,Looper 通过 MessageQueue 取消息,消息队列是先进先出模式,那我延迟发两个消息,第一个消息延迟2个小时,第二个消息延迟1个小时,那么第二个消息需要等3个小时才能取到吗?
鉴于这个血案,我们来翻翻案,一探究竟。
已知
- Main Handler 在 ActivityThread 的时候就 Looper.loop
- 所有的消息都是通过 Looper.loop 进行分发
求
- Message 消息队列对于延迟消息是如何处理的?
解
解题步骤氛围两步来看:
- 分发消息 sendMessageDelayed
- 接收消息 dispatchMessage
分发消息
Handler.class
public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
....
}
复制代码
Handler 在发送消息时都会进入这一步,从这段代码中我们捋出几个重要点:
- delay 设置的延迟时间低于0时默认为0
- uptimeMillis 为当前
时间戳+延迟时间
(注意,这里后面需要用上)
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
复制代码
最终会调用到 enqueueMessage ,这里给几个信息:
- msg.target 指当前创建的 Handler
- mAsynchronous 默认为 false
- 最终调用 MessageQueue.enqueueMessage
来看看 MessageQueue.enqueueMessage 干了啥:
MessageQueue.class
boolean enqueueMessage(Message msg, long when) {
...
synchronized (this) {
...
msg.when = when;
Message p = mMessages;
boolean needWake;
//① 如果进来的消息 when 比当前头节点 p.when 还小,就想该消息插入到表头
if (p == null || when == 0 || when < p.when) {
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
...
Message prev;
for (;;) {
prev = p;
//遍历链表
p = p.next;
//②
//p==null : 只有在遍历到链表尾的时候才会为 true
//when < p.when : 上一个消息的延迟大于当前延迟,这个地方就可以回顾面试的那个问题
//p.when 当做第一个延迟2小时,when 当做目前进来的延迟1小时,这个时候是为 true
if (p == null || when < p.when) {
break;
}
...
}
//③
msg.next = p;
prev.next = msg;
}
}
复制代码
继续捋关键点:
-
时间戳+延迟时间
在这个地方变成了when
,并且赋值给了 Message - 其他解释看标记处
这个地方需要重点讲解 ③ 处,这个地方要分类去讨论,我们给出两个假设和例子:
假设一: p==null 为 true
p==null
为 true 的话,也就意味着链表遍历到了链尾,并且 when < p.when
一直都为 false,也就是说进来的消息延迟都是大于当前节点的延迟,这个地方我们来举个满足条件例子:
- 原消息链:0s -> 0s -> 1s -> 4s
- 进来延迟消息为 10s
最后的代码就是意思就是 10s.next=null
、4s.next=10s
,最终链表为:
- 0s -> 0s -> 1s -> 4s -> 10s
假设二: when < p.when 为 true
也就是说,链表还没有遍历到链尾发现进来的消息延迟小于当前节点的延迟,然后break了循环体,这个地方也来举一个满足条件的例子:
- 原消息链:0s -> 0s -> 1s -> 4s
- 进来延迟消息为 2s
遍历到 4s 的时候,发现 2s < 4s,break,当前 p 节点指向的是节点 4s,则最后代码的意思就是 2s.next=4s
、1s.next=2s
,最终链表为:
- 0s -> 0s -> 1s -> 2s -> 4s
总结
Handler 会根据延迟消息整理链表,最终构建出一个时间从小到大的序列
接收消息
Looper.class
public static void loop() {
final MessageQueue queue = me.mQueue;
for (;;) {
Message msg = queue.next(); // might block
...
try {
msg.target.dispatchMessage(msg);
}catch()
}
...
}
复制代码
loop 会一直循环去遍历 MessageQueue 的消息,拿到 msg 消息后,会将消息 dispatchMessage 发送出去,那么,me.next() 取消息就显得尤为重要了,我们进来看看。
MessageQueue.class
Message next() {
...
int nextPollTimeoutMillis = 0;
for (;;) {
...
synchronized (this) {
// Try to retrieve the next message. Return if found.
//①、获取当前的时间戳
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
...
if (msg != null) {
//②,如果当前时间戳小于所取延迟消息,则以他们的时间差返回
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;
}
} else {
// No more messages.
nextPollTimeoutMillis = -1;
}
...
nextPollTimeoutMillis = 0;
}
}
复制代码
详细解释下:
②标识:
还记得 msg.when
是由什么构成的嘛?时间戳+delay
,每次循环都会更新 now
的时间戳,也就是说,当前for循环会一直去执行,直到 now
大于 时间戳+delay
就可以去取消息了。
④标识:
因为消息的存取都是按时间从小到大排列的,每次取到的消息都是链表头部,这时候链头需要脱离整个链表,则设置 next=null。知道最后这个用完的消息去哪了嘛?还记得 obtainMessage 复用消息吗?
总结
延迟消息的发送是通过循环遍历,不停的获取当前时间戳来与 msg.when 比较,直到小于当前时间戳为止。那通过这段代码我们也是可以发现,通过 Handler.delay 去延迟多少秒是非常不精确的,因为相减会发生偏差
答
回顾问题,我们来解答:
- MessageQueue 的实现不是队列,不要被名称迷惑,他是一个链表
- 每次发送消息都会按照 delay 从小到大进行重排
- 所有的 delay 消息都是并行的,不是串行的
- 第一个延迟2个小时,第二个延迟1小时,会优先执行第二个,再过1小时执行第一个
网友评论