MessageQueue 现状
-
全局锁竞争与优先级反转 (Priority Inversion)
- 现状:主线程(Looper)拿消息、子线程发消息、或者删消息时,全都要抢同一把锁。
- 劣势:如果某个低优先级的后台网络线程正在疯狂发消息(占用了锁),此时主线程正好收到了底层极其高优的 VSYNC 信号准备进行 UI 绘制(
doFrame),主线程会被迫在锁外排队等待。这直接导致了帧率截止时间的错失,引发明显的界面卡顿(Jank)。
-
O(N) 的插入与遍历损耗
- 现状:当发来一个指定执行时间(
when)的消息时,系统必须加着锁,从头开始遍历单向链表,直到找到合适的位置插入。 - 劣势:当队列中积压了大量消息时,每次插入的时间复杂度都是 O(N)。且在遍历期间,整个系统处于锁死状态。
- 现状:当发来一个指定执行时间(
-
丑陋且低效的同步屏障 (SyncBarrier)
- 现状:为了让 UI 绘制消息优先执行,旧版会在链表头插一个 target 为 null 的“屏障节点”。
- 劣势:主线程在循环时,一旦看到屏障,就必须在链表里使用
while循环不断往后找,直到找到第一个标记为异步的消息。这种寻找也是 O(N) 复杂度的,拖慢了高优消息的调度速度。
-
危险且复杂的移除操作 (removeMessages)
- 现状:为了从队列中删掉特定的消息,必须加锁遍历整个链表,找到后还要小心翼翼地修改前后节点的指针使其断开。高并发下极其容易出错或死锁。
二、数据结构解析
DeliQueue/MessageQueue.java
源码:cs.android.com/android/pla…
public final class MessageQueue {
private static final String TAG = "DeliQueue";
private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
...
...
MessageStack mStack = new MessageStack();
}
MessageStack.java
源码:frameworks/base/core/java/android/os/MessageStack.java
public final class MessageStack {
private static final String TAG = "MessageStack";
private static final VarHandle sTop;
private volatile Message mTopValue = null;
private static final VarHandle sFreelistHead;
private volatile Message mFreelistHeadValue = null;
// The underlying min-heaps that are used for ordering Messages.
private final MessageHeap mSyncHeap = new MessageHeap();
private final MessageHeap mAsyncHeap = new MessageHeap();
// This points to the most-recently processed message. Comparison with mTopValue will indicate
// whether some messages still need to be processed. This value excludes the quitting sentinel.
private Message mLooperProcessed = null;
...
...
}
mSyncHeap 与 mAsyncHeap (双堆架构)
在旧版的 LegacyMessageQueue 中,为了保证界面的高优先级渲染,Google 发明了“同步屏障(SyncBarrier)”:一旦开启屏障,只有打上了 setAsynchronous(true) 标记的异步消息(通常是 Choreographer 的 doFrame 等界面绘制消息)才能被优先执行,普通同步消息会被阻塞。这种逻辑在单向链表中需要用大段的 while 循环去判断,极易导致死循环和锁竞争。
在新的架构中,Google 用“结构设计”替代了“算法逻辑”:
mSyncHeap(同步消息最小堆) :主线程维护的用于存放所有普通/同步消息的优先级队列(按when排序)。mAsyncHeap(异步消息最小堆) :专门用于存放通过setAsynchronous(true)标记的高优先级异步消息。
当主线程在 MessageQueue.next() 中挑选下一个要执行的消息时,它不再需要去判断什么“同步屏障标记节点”了。它只需要做极其简单的数据结构判决:
- 检查是否存在同步屏障(现在只是一个标志位)。
- 如果存在,直接去
mAsyncHeap的堆顶拿消息,完全忽略mSyncHeap。 - 这种双堆隔离设计,让主线程寻找最高优渲染消息的时间复杂度降到了 O(1),极其高效。
mTopValue (无锁状态核心标识)
mTopValue 实际上指向了并发栈 mStack 的栈顶元素(Top Node) 。
- 它就是 Google 提到的 Treiber Stack(无锁并发栈) 的最核心指针。所有多线程高并发调用
sendMessage时,底层执行的 CAS(Compare-And-Swap) 原子操作,抢夺和替换的就是这个mTopValue的引用。 - 只要一个子线程成功把自己的
Message挂到了mTopValue上,并且把原先的mTopValue设为了自己的next,压栈就成功了。 - 它的类型是
Message(它就是一个消息链的起点,只不过这个链是临时的,随时会被主线程一把抓走)。
mLooperProcessed (排空游标 / 水位线)
在传统的队列中,如果你要把一个消息从“接收区”移到“工作台”,你需要做物理删除(把节点的指针断开),这必不可免地需要加全局锁,否则子线程刚好在这个时候插消息,链表就断了。
但在 DeliQueue 中,为了绝对的无锁,主线程(Looper)绝对不会去修改/断开并发栈上的链表结构! 它只做只读遍历。
既然不断开链表,那主线程怎么知道哪些消息是“新来的”,哪些消息是“上一轮已经拿去 Heap 里的”呢?
答案就是 mLooperProcessed (Looper 已处理游标) 。它就是一个“书签”,记录了主线程上一次扫描到了哪里。
三、场景逻辑推演
消息生产阶段
我们假设这 4 个消息中有异步的绘制消息和普通的同步消息:
Msg1(普通同步消息)Msg2(异步渲染消息,带有setAsynchronous(true))Msg3(普通同步消息)Msg4(异步紧急消息,带有setAsynchronous(true))
以下是加入了双堆(Dual-Heap)变化的完整推演过程:
初始状态:系统刚启动
-
入口与书签:
mTopValue=null(无锁并发栈为空)mLooperProcessed=null(主线程还没看过任何消息)
-
消费者工作台 (双堆) :
mSyncHeap=[](普通消息堆为空)mAsyncHeap=[](异步消息堆为空)
第一轮:子线程发消息 (高并发入栈)
-
子线程 A 发送
Msg1(普通消息):- 利用 CAS,让
Msg1.next = null,然后更新mTopValue = Msg1。
- 利用 CAS,让
-
子线程 B 发送
Msg2(异步消息):- 利用 CAS,让
Msg2.next = Msg1,然后更新mTopValue = Msg2。
- 利用 CAS,让
📝 此时的数据结构状态:
mStack结构:mTopValue -> [Msg2(异)] -> [Msg1(同)] -> nullmSyncHeap:[](未搬运)mAsyncHeap:[](未搬运)
第二轮:主线程 (Looper) 醒来,开始搬运与分拣 (heapSweep)
主线程进入 MessageQueue.next(),立刻执行 heapSweep:
-
确定区间: 主线程看到当前
mTopValue是Msg2,自己的书签mLooperProcessed是null。 -
顺藤摸瓜并分拣 (从
Msg2开始向下遍历,遇到null停止):- 抓取
[Msg2(异)],发现它带有异步标记,执行SiftUp插入mAsyncHeap。 - 往下看,抓取
[Msg1(同)],发现它是普通同步消息,执行SiftUp插入mSyncHeap。 - 再往下看,遇到
null,等于书签mLooperProcessed,立刻停止扫描!
- 抓取
-
更新书签: 主线程把刚刚扫描起点的
Msg2记作新书签:mLooperProcessed = Msg2。
📝 此时的数据结构状态:
mStack结构:mTopValue -> [Msg2(异)] -> [Msg1(同)] -> null(主线程不修改指针,保留原状)mSyncHeap:[ Msg1 ](按 when 排序)mAsyncHeap:[ Msg2 ](按 when 排序)
随后,主线程根据是否有 SyncBarrier,从相应的 Heap 堆顶取出消息执行。
第三轮:子线程又发新消息
在主线程执行上面消息的过程中,其他线程又发来了新消息:
-
子线程 C 发送
Msg3(普通消息):- 利用 CAS,让
Msg3.next = Msg2,然后更新mTopValue = Msg3。
- 利用 CAS,让
-
子线程 D 发送
Msg4(异步消息):- 利用 CAS,让
Msg4.next = Msg3,然后更新mTopValue = Msg4。
- 利用 CAS,让
📝 此时的数据结构状态:
mStack结构:mTopValue -> [Msg4(异)] -> [Msg3(同)] -> [Msg2(异)] -> [Msg1(同)] -> null- (假设 Heap 里的消息由于未到触发时间,还暂存在工作台上)
第四轮:主线程 (Looper) 再次醒来搬运
主线程执行完手头的活,再次进入 MessageQueue.next() 执行 heapSweep:
-
确定区间: 主线程看到最新的
mTopValue变成了Msg4。书签mLooperProcessed是Msg2。 -
顺藤摸瓜并分拣 (从
Msg4开始向下遍历,遇到Msg2停止):- 抓取
[Msg4(异)],它是异步消息,执行SiftUp插入mAsyncHeap。 (此时mAsyncHeap会根据Msg4和Msg2的触发时间重新排序) 。 - 往下看,抓取
[Msg3(同)],它是普通同步消息,执行SiftUp插入mSyncHeap。 (此时mSyncHeap会根据Msg3和Msg1的时间排序) 。 - 再往下看,发现是
[Msg2(异)]。 - 拦截生效:
Msg2等于书签mLooperProcessed,主线程知道:Msg2和它后面的Msg1,我上一轮已经分拣过了,立即停止扫描!
- 抓取
-
更新书签: 主线程把最新的起点
Msg4记作新书签:mLooperProcessed = Msg4。
📝 最终的数据结构状态:
mStack结构:依然保持那一串长长的链表 (旧节点会随着内存回收机制逐步被清理,我们这里不深究释放过程)。mSyncHeap:[ Msg1, Msg3 ](内部已按 when 的时间先后排好序)mAsyncHeap:[ Msg2, Msg4 ](内部已按 when 的时间先后排好序)
核心变化
加上双堆变化后,能明显感觉到这套架构的强悍之处: 多线程把无序的、什么类型都有的消息“一锅粥”地全倒在 mTopValue 上。而主线程就像一个极其熟练的图书管理员,通过 mLooperProcessed 游标精准地拿到新书,然后有条不紊地把它们分别塞进“同步书架(mSyncHeap)”和“异步书架(mAsyncHeap)”。
当 UI 渲染的信号(VSYNC)到来需要开启同步屏障时,主线程连看都不看同步书架一眼,直接去异步书架拿书,没有丝毫的迟疑与遍历的损耗!
消息消费阶段
📝 前情提要,此时主线程的工作台状态:
mSyncHeap:[ Msg1(同), Msg3(同) ](内部已按 when 时间先后排好序,假设 Msg1 最早)mAsyncHeap:[ Msg2(异), Msg4(异) ](内部已按 when 时间先后排好序,假设 Msg2 最早)
第五轮:消费阶段 - 出堆与决策 (The Poll Phase)
主线程收割完新消息后,开始从工作台拿取消息执行。
场景 1:当前没有同步屏障 (Normal Mode)
-
对比堆顶:主线程看到桌子上两堆书。它取出
mSyncHeap的最顶层(最早)消息Msg1,和mAsyncHeap的最顶层消息Msg2。 -
公平竞争:比较
Msg1.when和Msg2.when,谁的时间更小(谁该先执行)就选谁。假设Msg1时间更早。 -
弹出堆顶:从
mSyncHeap弹出Msg1。 -
墓碑与时间校验:
-
检查
Msg1是否被标记为墓碑(Tombstone,即被调用了removeMessages取消)? -
如果没有被取消,检查
Msg1.when是否小于等于当前时间?
-
-
返回执行:如果一切正常,主线程的
MessageQueue.next()将Msg1递交给Looper,Looper 随后调用Msg1.target.dispatchMessage()执行代码。
📝 消费后状态:
mSyncHeap:[ Msg3(同) ],mAsyncHeap:[ Msg2(异), Msg4(异) ]
场景 2:当前开启了同步屏障 (SyncBarrier Mode)
前提:此时底层收到了 VSYNC 信号,即将要开始进行极其重要的 UI 绘制,为了保证不掉帧,系统向消息队列插入了一个“同步屏障”节点(或设置了屏障标志)。
-
直接抄近道:主线程发现有同步屏障!它直接无视
mSyncHeap(哪怕里面的Msg3其实早就过期了该执行了)。 -
单推异步堆:主线程只盯着
mAsyncHeap看,取出最顶层的Msg2。 -
弹出堆顶:从
mAsyncHeap弹出Msg2。 -
墓碑与时间校验:
- 检查
Msg2是不是墓碑?不是。 - 检查
Msg2.when到时间了吗?到了。
- 检查
-
返回执行:主线程将
Msg2递交给Looper,去执行极其关键的界面渲染工作。
📝 消费后状态:
mSyncHeap:[ Msg3(同) ](继续被阻塞),mAsyncHeap:[ Msg4(异) ]
第六轮:消费阶段 - 无事可做与休眠 (The Park Phase)
假如主线程飞速执行完了 Msg1 和 Msg2,现在又回来 MessageQueue.next() 找事情做。 (假设此时没有新消息发来, mLooperProcessed 也没有变化) 。
-
对比堆顶:主线程对比剩下的
Msg3和Msg4。 -
发现未到期:假设虽然选出了
Msg3,但是它规定的执行时间是在 2 秒后。 -
计算时差与休眠 (epoll_wait) :
- 主线程更新自身状态标志为
STACK_NODE_TIMEDPARK(定时休眠)。 - 计算距离
Msg3.when还有 2 秒,调用底层nativePollOnce(ptr, 2000)进入休眠,交出 CPU 控制权。
- 主线程更新自身状态标志为
第七轮:消费阶段 - 墓碑机制发威 (Tombstone Deletion)
这里补充一个在旧版极难处理,而在 DeliQueue 中极为优雅的场景:取消消息。
假设在上面的休眠期间,由于用户退出了当前页面,业务代码调用了 handler.removeMessages(Msg4.what)。
- 标记墓碑 (惰性删除) : 系统不会去遍历
mAsyncHeap找到Msg4然后把它删掉(因为这会破坏堆结构,且极其低效)。系统只会在底层的数据标记上,将匹配的Msg4设为 Tombstone(墓碑/已取消) 。 - 休眠结束,醒来干活: 时间到了,主线程醒来。假设此时终于轮到
Msg4在mAsyncHeap的最顶端了。 - 弹出并校验:主线程弹出
Msg4。在进入执行前的“墓碑校验”环节,发现:哦?这个Msg4已经被立了墓碑。 - 直接丢弃:主线程二话不说,直接把
Msg4当垃圾丢进内存回收池(Freelist),不将它递交给 Looper。 - 继续循环:主线程重新去对比如今堆顶的消息,寻找下一个合法受害者。
核心变化
看完整个消费阶段,可以发现 DeliQueue 最大的两点颠覆:
- 同步屏障被“物理结构化”了:过去是在一条链表里用
while循环苦苦寻找带异步标记的消息(O(N)),现在是两个完全分离的书架,找异步消息就是直接拿最上层那本(O(1))。 - 删除操作被“惰性化”了:过去为了删一个消息需要遍历链表、断开前后指针,还得加锁防并发。现在仅仅是贴个“墓碑”标签,等这个消息浮出水面准备执行时,直接像弹飞灰尘一样弹掉。
四、流程图解析
graph TD
classDef stack fill:#fff3e0,stroke:#f57c00,stroke-width:2px,color:#000000;
classDef process fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#000000;
classDef heap fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#000000;
classDef loop fill:#f3e5f5,stroke:#8e24aa,stroke-width:2px,color:#000000;
classDef decision fill:#e8eaf6,stroke:#3949ab,stroke-width:2px,color:#000000;
%% 主入口
Start(("主线程 Looper 调用 MessageQueue.next")):::loop --> CallSweep
%% heapSweep 流程
subgraph HeapSweep ["🧹 heapSweep 转移过程"]
CallSweep["执行 mStack.heapSweep()"]:::process
ReadTop["读取当前 mTopValue 指针(获取此刻最新栈顶)"]:::process
CallSweep --> ReadTop
SetCurrent["设置游标: current = mTopValue"]:::process
ReadTop --> SetCurrent
CheckEnd{"current == mLooperProcessed? 或者 current == null?"}:::decision
SetCurrent --> CheckEnd
CheckEnd -- "No (发现未处理的新消息)" --> ProcessNode["处理 current 节点"]:::process
ProcessNode --> CheckAsync{"current.isAsynchronous()"}:::decision
CheckAsync -- "Yes (是异步高优消息)" --> SiftAsync["SiftUp 插入排序到 mAsyncHeap"]:::heap
CheckAsync -- "No (是普通同步消息)" --> SiftSync["SiftUp 插入排序到 mSyncHeap"]:::heap
SiftAsync --> NextNode
SiftSync --> NextNode
NextNode["current = current.next (沿着单向链表往下看)"]:::process
NextNode --> CheckEnd
CheckEnd -- "Yes (扫到水位线了,增量收割完毕)" --> FinishSweep["更新水位线: mLooperProcessed = mTopValue"]:::process
end
%% 出堆消费流程
subgraph HeapConsume ["双堆消费过程(单线程安全)"]
FinishSweep --> CheckBarrier{"当前存在 SyncBarrier (同步屏障) 吗?"}:::decision
CheckBarrier -- "Yes (拦截普通消息)" --> PollAsync["只从 mAsyncHeap 获取堆顶 (最早) 消息"]:::heap
CheckBarrier -- "No (公平竞争)" --> CompareHeaps["对比 mAsyncHeap 和 mSyncHeap 堆顶的 when 时间"]:::heap
CompareHeaps --> PollWinner["弹出两者中 when 最小的那个消息"]:::heap
PollAsync --> PostProcess
PollWinner --> PostProcess
PostProcess["判断时间、过滤 Tombstone 最终返回给 Looper 执行"]:::loop
end
五、时间复杂度
| 旧版 MessageQueue | DeliQueue | |
|---|---|---|
| 插入 | O(N) | O(1) |
| 移除 | O(1) | O(logN) |
| 锁竞争 | 高 | 无 |