作为 Android 基础架构的核心部分,消息机制(Message Queue)负责处理线程间通信及 UI 更新。尽管相关文档众多,但许多开发者对底层细节的理解仍停留在表面。本文基于 Android Q (10.0) 源码,深入剖析消息机制中几个常被忽视的冷门知识点,涵盖消息排序、IdleHandler 实现原理、同步屏障行为以及性能监控指标。
1. 消息入队顺序与时间戳机制
假设线程 1 正在处理消息,线程 2 通过 Handler 向该线程的消息队列插入两个消息 A 和 B。我们需要明确它们的执行顺序取决于发送方式。
场景一:使用 sendMessage 系列方法
handler.sendMessage(msgA);
handler.sendMessage(msgB);
在此模式下,消息 A 会先于消息 B 被处理。这是因为 sendMessage 最终调用 enqueueMessage,传入的时间戳 uptimeMillis 为当前系统时间。由于消息 B 的发送晚于 A,其获取到的时间戳通常大于 A。在 MessageQueue 的链表插入逻辑中,若时间戳相同,后插入的消息排在后面;若时间戳不同,按时间戳从小到大排序。因此 A 在前。
场景二:使用 sendMessageAtFrontOfQueue
handler.sendMessageAtFrontOfQueue(msgA);
handler.sendMessageAtFrontOfQueue(msgB);
此时消息 B 会先于消息 A 被处理。该方法内部调用 enqueueMessage 时,将 uptimeMillis 强制设为 0。在源码逻辑中,当 when == 0 时,新消息会被直接插入到队列头部,覆盖原有头节点的位置。因此,后调用的 B 会抢占 A 的位置,导致 B 优先执行。
源码分析
// MessageQueue.enqueueMessage 核心逻辑片段
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...
// ...遍历找到合适位置
}
上述代码表明,只要 when 为 0,无论队列状态如何,新消息都会成为新的头节点。这解释了为何连续调用 sendMessageAtFrontOfQueue 会导致后进先出(LIFO)的效果。
2. IdleHandler 数组拷贝的必要性
在 Looper 循环中,当消息队列为空时,会触发 IdleHandler。源码中有一处关键操作:
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
为什么不能直接遍历 ArrayList 类型的 mIdleHandlers?
原因一:线程安全
mIdleHandlers 的修改(如移除元素)必须持有 MessageQueue 对象的同步锁(synchronized(this))。如果直接在同步块内遍历并执行 queueIdle(),一旦某个 Handler 耗时过长,其他线程尝试发送消息时将因无法获取锁而阻塞。

