Android 17 的 MessageQueue 改动,发生在消息交给 Looper 的协调路径上。Handler API 和 Looper 的消费模型没有变。
单锁链表实现与 Deli 是同一个 android.os.MessageQueue 的两条内部路径,不是两套公开 API,也不是同一个 Looper 上同时消费的两条队列。单锁链表实现让生产者和 Looper owner 共同维护有序链表;Deli 让生产者通过并发提交结构交接消息,再由 Looper owner 整理和选择。CombinedDeliMessageQueue 承载这两条路径,进程最终走哪一条由版本、构建和运行选择决定。
应用入口仍然是熟悉的代码:
kotlin
handler.post {
// 由 Handler 绑定的 Looper 线程执行
}
Android 官方说明,在 Android 17 上,target API 37 及以上的应用默认启用新的 lock-free MessageQueue;Handler 和 Looper 的公开用法保持不变。影响集中在内部实现、私有反射和测试工具。〔1〕
单锁链表实现让生产者和 Looper 共同维护一条由对象锁保护的有序链表。DeliQueue 把这段工作分成两步:生产者通过 CAS 提交消息,Looper owner 负责排序、清理和计算下一次等待时间。变化集中在消息进入队列与选择下一条消息之间;callback 仍按原来的串行方式执行。
这项改动只影响这段路径:慢 callback、I/O 或业务锁仍会占用主线程;DeliQueue 针对的是后台线程入队时与 Looper 争用同一把队列锁的协调成本。
正文以 Android 17 r1 为主线,Deli 是当前实现,单锁链表实现用于对照。Android 16 的 Combined/Concurrent 只在扩展阅读中交代位置,不展开其内部实现。
1. 不变的是 Looper 的消费模型
"主线程消息队列"这个说法容易让人忽略:MessageQueue 可以接收多个线程提交的消息,但只有绑定它的 Looper 线程负责取消息和分发。
主线程的 Looper 只是最常见的实例。HandlerThread 或自行调用 Looper.prepare() 的线程也遵守同一条规则:多个 Handler、多个生产者线程可以把消息送进同一个队列,消息最终由创建该 Looper 的 owner 线程串行执行 callback。
Android 17 r1 的 Looper.loopOnce() 仍保留这条主路径:〔2〕
java
Message msg = queue.next();
msg.target.dispatchMessage(msg);
queue.next() 选出一条可执行消息,dispatchMessage() 随后在当前 Looper 线程调用目标 Handler。DeliQueue 可以减少不同生产者之间的阻塞,但消费者仍然只有一个。这里的 lock-free 只描述队列的并发提交路径,不描述 callback 的执行模型。
Java 与 native 仍按职责分工:Java MessageQueue 负责消息时间、同步屏障、异步标记、移除和退出等语义;暂时没有可执行消息时,owner 线程会从 next() 进入 nativePollOnce() 等待。JNI 调用不会自动切换线程;等待结束后,原来的 owner 回到 Java 层继续分发。另一个生产者线程可以在入队后调用 nativeWake() 唤醒它,但不能代替它执行 callback。
两种实现共享同一个角色模型:生产者把消息交给队列,Looper owner 决定何时取出并执行。单锁链表实现的瓶颈来自共享锁------生产者和 owner 都要在这把锁下维护同一条有序链表。

2. 单锁链表实现:所有参与者共用一把锁
单锁链表实现用 mMessages 保存按执行时间排列的单链表。生产者调用 enqueueMessage() 时,不只是把消息交给队列,还要在对象锁内找到插入位置并改好前后节点;Looper 调用 next() 时,也要取得同一把锁,判断队首是否到期、同步屏障后有没有异步消息,以及下一次应等待多久。
链表头是最近需要处理的消息,when 决定时间顺序,mLast 为队尾追加保留快速路径。队首插入和部分队尾追加不需要遍历,中间插入才会沿链表寻找位置;遇到同步屏障时,next() 还要继续向后寻找可执行的异步消息。问题不在于每次链表操作都要 O(n),而在于这些不同成本的操作都在同一个锁域里完成。
Android 17 r1 的单锁链表实现源码展示了两个共享点:enqueueMessage() 在 synchronized (this) 中维护有序链表,next() 从 native poll 返回后也进入 synchronized (this) 检查队列。消息查询、移除和屏障维护同样需要和这份链表状态协调。〔3〕
这会产生一个与 callback 无关的等待:后台线程正在锁内扫描并插入一条定时消息,Looper 恰好执行完上一条 callback,准备进入 next()。此时即使队列里已经有到期消息,Looper 也要先等生产者释放对象锁。持锁时间越长,或者同时入队、查询、移除的线程越多,这个共享点越容易暴露。
只有持锁线程的实际调度优先级低于等待它的 Looper 线程时,这段等待才进一步构成优先级反转。"后台线程"只是代码角色,不等于较低的 Linux 调度优先级;具体进程仍要靠线程优先级和 trace 证明。Android 官方把这套单锁链表实现的风险概括为单 monitor 带来的锁竞争及优先级反转,不是每次入队都会让主线程卡顿。
共享锁只是表面现象,更深一层的问题是职责:生产者不仅提交消息,还参与最终有序结构的维护。DeliQueue 改动的正是这条边界。

3. Deli:生产者提交,Looper 整理
Android Developers 对 DeliQueue 的技术说明把新结构概括为两部分:生产者写入 Treiber stack,优先队列由 Looper 线程独占。前者是一种通过 CAS 更新栈顶的并发提交结构,后者负责真正的时间排序和消息选择。〔4〕
第一段发生在生产者线程。Android 17 r1 的 enqueueMessageUnchecked() 为消息分配插入序号,再把消息压入 MessageStack。生产者不需要取得单锁链表实现的对象锁,也不负责在完整有序结构中寻找最终位置。提交后,它通过原子等待状态更新 deadline 或计数;如果新消息改变了 owner 的睡眠条件,再执行唤醒。〔5〕
第二段发生在 Looper owner 调用 nextMessage() 时。owner 通过 heapSweep() 把提交栈中的消息取出并整理,把同步消息和异步消息分别放入最小堆,再从当前允许执行的候选中选出下一条。排序依据来自执行时间和插入序号,而不是消息在提交栈里的物理位置。这些关系分别落在固定版本的 MessageStack.java、MessageHeap.java 和 DeliQueue README 中。〔6〕〔7〕〔8〕
假设两个生产者依次生成三条消息:A(when=100, seq=1)、B(when=90, seq=2)、C(when=100, seq=3)。提交栈在 owner 接手前可能是:
text
C → B → A
整理完成后,下一条候选仍按时间和序号排列:
text
B → A → C
提交栈允许暂时后进先出,是因为它只负责把消息安全地交给 owner;最终执行顺序由 owner 独占的堆决定。把这两个阶段混成一个"新队列改成栈"的描述,会得到相反的顺序结论。
移除沿用同样的所有权划分。其他线程先把匹配消息标记为 tombstone 并清理引用,owner 在后续整理或堆维护中完成物理清理。这样做避免生产者为了摘除节点重新争夺一份全局有序结构,但 owner 要承担更多整理工作。原有的全局 Message 对象池也不再用于 Deli 内部复用,以免墓碑节点和并发观察发生 ABA 式混淆;Message.obtain() 的公开调用方式没有因此消失。〔8〕〔12〕
等待和唤醒也由 owner 协调。WaitState 记录会影响睡眠的 deadline、计数和屏障状态,生产者只在自己的提交确实改变等待条件时唤醒 owner。没有可执行消息时,最终进入 native poll 的仍是这条 Looper 线程。
提交、整理和清理虽已拆开,MessageQueue 的外部规则仍要保持:定时消息的顺序、sendMessageAtFrontOfQueue()、同步屏障、异步消息和 remove 语义,都需要在这套新结构上重新成立。

4. 顺序、屏障与移除的语义保持
栈和堆改变的是内部组织,调用者仍然要看到同一组结果:哪条消息先执行,队首插入能否到达队首,同步屏障出现时谁可以通过,移除后消息何时不再可见。
| 操作 | 外部语义 | Deli 中承担这项工作的结构 |
|---|---|---|
| 普通定时消息 | 按 when 选择;同一时间点保留插入相对顺序 |
insertSeq 与 owner 独占的最小堆 |
| 队首插入 | 消息进入队首语义,不被普通定时消息直接改写 | 特殊的 front 序号与候选比较 |
| 同步屏障 | 屏障存在时,同步消息暂不成为候选;异步消息仍可通过 | 同步/异步双堆与屏障状态 |
removeMessages() |
匹配消息不再被后续 next() 取出 |
逻辑标记、引用清理和 owner 的物理整理 |
quit() / 等待 |
队列退出和等待/唤醒仍由 Looper owner 协调 | wait state、native wake 和退出状态 |
提交栈暂时呈现 LIFO,并不等于普通消息按 LIFO 执行;MessageHeap 只是 owner 使用的排序结构,单看它也无法证明屏障语义。最终顺序由 when、插入序号、front 规则和屏障候选共同决定。
移除的边界在于:单锁链表实现可以在锁内把节点从链表摘掉;Deli 让其他线程先标记 tombstone,owner 再完成堆和提交结构的物理整理。对调用者来说,消息不应再被取出;对实现来说,节点何时真正断链和回收被延后了。Deli 因此不再把原有的全局 Message pool 当作内部复用中心,但公开的 Message.obtain() 仍然存在。〔8〕〔12〕
外部语义与内部观测是两件事。Android 官方文档明确说明,Deli 为二进制兼容保留 mMessages,但它始终是 null;依赖这个字段、私有链表或旧版 looper 假设的工具不能继续把它当作队列状态。〔1〕源码和 framework/CTS 测试入口可以帮助核对这些语义,但本地没有执行 AOSP 构建、CTS 或真机回归,测试入口不等于测试已经通过。〔9〕
5. 性能收益集中在队列协调路径
一条消息从提交到 callback,经过四段:
text
提交协调 → 等待 / 唤醒 → 取下一条消息 → callback 执行
单锁链表实现把前两段的一部分工作和链表维护绑在对象锁上。生产者中间插入、查询或移除的时间,会直接变成 Looper 申请同一把锁的等待时间。Deli 把生产者的第一步改成 CAS 提交,排序和物理清理由 owner 继续完成,因此减少了生产者与 owner 维护同一份有序核心状态时的直接争用。
成本没有消失,只是重新分配:生产者少做一次完整有序插入,owner 需要整理提交栈、维护两个最小堆、处理墓碑和计算下一次等待。消息很多、屏障频繁或 remove 很多时,owner 仍然要承担这些工作。lock-free 描述的是 Deli 的并发提交路径,不能推导出整个事件循环没有锁,也不能推导出所有队列操作都比单锁链表实现更快。
callback 仍是独立阶段。耗时 Runnable、布局绘制、I/O、Binder 调用或业务锁,继续占用 Looper owner;它们不会因为生产者提交改成 CAS 就自动变短。遇到卡顿时,可以先按阻塞位置区分:
- 卡在单锁链表实现的队列锁或跨线程入队协调,查生产者、锁等待和调度优先级。
- 卡在
nativePollOnce(),查等待条件、deadline、唤醒和外部 FD 事件。 - 已经进入
dispatchMessage()且执行时间长,查 callback、业务锁、I/O 或 Binder。
Android Developers 的技术说明给出了合成入队测试和内部 beta 观测,也提供了 Perfetto 的诊断入口;这些数字的完整条件和基线没有公开,不能外推为每个应用的固定收益。这里没有独立真机 A/B 或 trace 结果,结论限于结构判断和官方材料的使用边界。〔4〕
6. 升级后的兼容检查
升级时,按代码是否依赖内部状态分流。只使用公开 Handler、Looper 和 MessageQueue API 的业务代码通常不需要重写消息逻辑;Android 17 target API 37 及以上时,系统默认把进程带到新实现,应用应按自己的依赖和测试方式完成回归。〔1〕
重点是盘点那些把单锁链表实现当成观测接口的代码:
| 场景 | 可能受影响的地方 | 处理方式 |
|---|---|---|
| 业务代码只调用公开 API | post、postDelayed、removeCallbacks 等调用方式没有改 |
保持业务代码,覆盖定时消息、跨线程提交、移除、退出和页面销毁回归 |
反射 mMessages 或其他私有字段 |
Deli 模式下 mMessages 为 null,单锁链表观察不再代表队列状态 |
删除私有反射,改用受支持的测试接口、dump 或 trace |
| Espresso UI 测试 | 旧版 idle 判断可能依赖传统队列结构 | 升级到 Espresso 3.7.0 或更高版本,使用其新的队列交互路径 |
| Robolectric 单元测试 | @LooperMode(LEGACY) 与新队列的时钟/空闲假设不同 |
升级到 Robolectric 4.17 或更高版本,迁移到 @LooperMode(PAUSED) |
| 自研测试或诊断工具 | 直接读取私有链表、假定队列节点可遍历 | 先在新实现路径上运行,再按公开行为重新定义断言和观测点 |
compat 开关适合做归因实验:在 Android 17 的 debuggable 应用上,可以用 adb am compat enable USE_NEW_MESSAGEQUEUE <package> 或 disable 对比两条路径。这个选择在进程创建 MessageQueue 之前确定,粒度是应用进程,不是某个 Handler 或某一条消息;它不是生产配置,也不是运行中的热切换。异常若只在 Deli 路径出现,先用回退确认边界,再修复私有依赖或升级测试工具。〔1〕〔5〕〔11〕
Android 16 的过渡实现
Android 16 是这次改造的过渡版本。Android 16 r4 保留单锁链表,只在部分系统进程中试用新的并发队列。到了 Android 17 r1,并发方案改为 Deli,同时继续保留单锁链表作为兼容路径。Android 16 的并发队列不在这里展开,相关入口见该版本的 CombinedMessageQueue/MessageQueue.java。〔10〕
Android 17 重做了消息提交与整理,Looper 的消费模型保持不变。Deli 让生产者通过 CAS 交接消息,把排序、清理和等待计算留给 owner。它减少的是 callback 之前的队列争用,callback 仍在同一 Looper 线程串行执行。对应用代码而言,公开的 Handler 和 Looper 用法通常不需要调整;迁移工作主要落在私有字段反射、测试工具和性能归因上。
参考资料
- Android 17 MessageQueue 行为变更说明
- Android 17 r1:Looper.java
- Android 17 r1:单锁链表 MessageQueue 实现
- Android Developers:Android 17 lock-free MessageQueue 技术说明
- Android 17 r1:CombinedDeliMessageQueue/MessageQueue.java
- Android 17 r1:MessageStack.java
- Android 17 r1:MessageHeap.java
- Android 17 r1:DeliQueue README
- Android 17 CTS:MessageQueueTest.java
- Android 16 r4:CombinedMessageQueue/MessageQueue.java
- Android 17 r1:ActivityThread.java
- Android 17 r1:Message.java