Android 的消息机制,从第一个版本到 Android 16,核心实现没怎么变过。
一个 synchronized 锁,守着一条单链表,所有线程排队等着往里塞消息。跑了二十年,终于在 Android 17 被重写了。
新的实现叫 DeliQueue,无锁架构,从数据结构到线程同步方式全部推倒重来。这篇文章聊聊它到底改了什么、为什么要改、以及你的 App 需要注意什么。

老架构出了什么问题
先回顾一下经典的 MessageQueue。
Handler 往主线程的 MessageQueue 发消息,Looper 循环取出来执行。整个过程用一把 synchronized 锁保护队列状态。
这在大多数情况下工作得很好。但有一个场景会出问题:多线程同时操作队列时,锁竞争会阻塞 UI 线程。
真实案例:Launcher 在拍完照回来后发生卡顿。分析发现,主线程在等 MessageQueue 的锁,而锁被一个后台线程持有。这个等待持续了 18ms------超过了 60Hz 刷新率下 16ms 的帧时间预算。一帧就这么丢了。
更麻烦的是优先级反转。低优先级线程拿到了锁,中优先级线程抢占了它的 CPU 时间片(但锁还在低优先级手上),高优先级的 UI 线程只能干等。三方僵持,卡顿就来了。
Google 用 Perfetto 在海量线上 trace 中分析后确认:MessageQueue 的锁竞争是系统级的普遍问题,不是某个 App 的个例。
新架构长什么样
DeliQueue 的设计思路很清晰:把「写入」和「消费」彻底分开,用不同的数据结构分别优化。
写入端:Treiber Stack
任何线程投递消息时,不用抢锁,直接用 CAS(Compare-And-Swap) 原子操作压入一个无锁栈。
java
public void push(E item) {
Node<E> newHead = new Node<E>(item);
Node<E> oldHead;
do {
oldHead = top.get();
newHead.next = oldHead;
} while (!top.compareAndSet(oldHead, newHead));
}
核心思路:先读取栈顶,把新节点接上去,然后用 CAS 尝试更新栈顶指针。如果被别的线程抢先改了,就重来。整个过程没有锁,O(1) 复杂度。
消费端:Min-Heap
Looper 线程从 Treiber Stack 中取出所有待处理的消息,塞进一个按截止时间排序的最小堆。因为只有 Looper 一个线程操作这个堆,完全不需要同步。
删除消息:墓碑标记
如果一个消息在被消费前需要取消怎么办?
DeliQueue 不会直接从数据结构里移除它(那需要同步),而是用 CAS 给它打上一个「已删除」标记(tombstone)。Looper 在下一次唤醒时统一清理。所有堆操作都在单线程内完成,零同步开销。
硬件加持:ARM LSE
在 ARMv8.1+ 处理器上,CAS 操作有专门的硬件指令支持(LSE,Large System Extensions),相比老的 LL/SC 方式,高竞争场景下快了约 3 倍。
现在市面上主流的 Android 手机基本都支持 LSE,这意味着 DeliQueue 在实际运行中能充分利用硬件优势。
性能对比
| 操作 | 旧架构 | DeliQueue |
|---|---|---|
| 消息投递 | O(N) 最差 | O(1) 无锁 |
| 取出队首 | O(1) | O(log N) |
| 锁竞争 | 有,可阻塞主线程 | 无 |
旧架构用单链表按时间有序插入,队列满的时候投递一条消息可能要遍历整条链。DeliQueue 反过来了------投递极快,排序的工作交给 Looper 在消费时做。
对于 UI 流畅度来说,投递端的延迟更关键(因为这是后台线程阻塞主线程的地方),所以这个取舍是值得的。
什么会坏
mMessages 字段永远返回 null
这是最直接的 Breaking Change。
旧实现里,MessageQueue.mMessages 指向链表头部,有些代码通过反射读这个字段来判断队列状态。在 Android 17 上,这个字段永远是 null,因为内部数据结构完全换了。
如果你的代码里有类似这样的操作:
java
// 这在 Android 17 上彻底不能用了
Field f = MessageQueue.class.getDeclaredField("mMessages");
f.setAccessible(true);
Object msg = f.get(queue); // 永远是 null
赶紧删掉。
Espresso 需要升级
Espresso 的旧版本通过反射 MessageQueue 内部状态来判断主线程是否空闲。新架构下这套机制失效了。
解决方案:升级到 Espresso 3.7.0+ ,新版本使用 TestLooperManager API,不依赖内部实现。
Robolectric Legacy 模式不能用了
如果你的测试里有 @LooperMode(LEGACY),在 Android 17 上会挂。
解决方案:升级到 Robolectric 4.17+ ,把 @LooperMode(LEGACY) 迁移到 @LooperMode(PAUSED)。
怎么提前测试
不需要升 targetSdk 就能验证。在 debug 包上执行:
bash
adb shell am compat enable USE_NEW_MESSAGEQUEUE your.package.name
如果发现问题,临时关掉:
bash
adb shell am compat disable USE_NEW_MESSAGEQUEUE your.package.name
也可以在开发者选项 → 应用兼容性变更里手动切换。
适配清单
- 搜索代码中对
MessageQueue私有字段的反射访问,全部移除 - Espresso 升级到 3.7.0+
- Robolectric 升级到 4.17+,LEGACY 模式迁移到 PAUSED
- 在 debug 包上用
USE_NEW_MESSAGEQUEUE开关提前测试 - 用 Perfetto 对比新旧架构下的帧耗时表现
写在最后
MessageQueue 是 Android 最底层的基础设施之一。能跑二十年不改,说明老架构设计得很好。而 DeliQueue 的出现,说明在多核时代,锁竞争已经成为不可忽视的性能瓶颈。
从 synchronized 到 Lock-Free,从单链表到 Treiber Stack + Min-Heap。这次重写不只是性能优化,更是 Android Runtime 底层架构思路的一次转向。
对绝大多数正常使用 public API 的 App 来说,这次改动是无感的------你的 App 会变得更流畅,不需要改一行代码。但如果你用了反射,现在该动手了。
你的项目里有反射 MessageQueue 的代码吗?评论区聊聊。