Android MessageQueue:从单锁队列到 DeliQueue

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 用法通常不需要调整;迁移工作主要落在私有字段反射、测试工具和性能归因上。

参考资料

  1. Android 17 MessageQueue 行为变更说明
  2. Android 17 r1:Looper.java
  3. Android 17 r1:单锁链表 MessageQueue 实现
  4. Android Developers:Android 17 lock-free MessageQueue 技术说明
  5. Android 17 r1:CombinedDeliMessageQueue/MessageQueue.java
  6. Android 17 r1:MessageStack.java
  7. Android 17 r1:MessageHeap.java
  8. Android 17 r1:DeliQueue README
  9. Android 17 CTS:MessageQueueTest.java
  10. Android 16 r4:CombinedMessageQueue/MessageQueue.java
  11. Android 17 r1:ActivityThread.java
  12. Android 17 r1:Message.java
相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone4 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen4 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone4 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui