Android17 为什么重写 MessageQueue

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 的代码吗?评论区聊聊。

相关推荐
阿巴斯甜1 天前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker1 天前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95271 天前
Andorid Google 登录接入文档
android
黄林晴1 天前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab2 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿2 天前
Android MediaPlayer 笔记
android
Jony_2 天前
Android 启动优化方案
android
阿巴斯甜2 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇2 天前
AOSP15 Input专题InputReader源码分析
android