Android UI 掉帧的问题
为了说明问题,让我们分析一个用户在拍照后导航回主屏幕时遭遇卡顿的 trace。下图展示了 Perfetto 中导致掉帧的事件序列:

- 现象:Launcher 主线程错过了帧截止时间。阻塞了 18ms,超过了 60Hz 渲染所需的 16ms 截止时间。
- 诊断:Perfetto 显示主线程被 MessageQueue 锁阻塞。”BackgroundExecutor” 线程持有该锁。
- 根因:BackgroundExecutor 运行在
Process.THREAD_PRIORITY_BACKGROUND(极低优先级)。它正在执行一个非紧急任务(检查应用使用限制)。与此同时,中优先级线程正在使用 CPU 处理来自相机的数据。OS 调度器抢占了 BackgroundExecutor 线程来运行相机线程。
这一序列导致 Launcher 的 UI 线程(高优先级)被相机工作线程(中优先级)间接阻塞——因为后者阻止了 Launcher 的后台线程(低优先级)释放锁。
问题拆解
要拆解这个问题,首先要搞清楚具体去掉了哪一把锁
在 Android 17(API 级别 37)中,Google 对 MessageQueue 进行了重大重构,引入了名为 DeliQueue 的无锁实现。
你问的具体是去掉了哪把锁?答案是:去掉了原本包裹在 enqueueMessage()、next()、removeMessages() 等核心方法周围的那个唯一的全局同步锁(Object Monitor Lock)。
以下是详细的改动逻辑和背后的机制:
1. 被去掉的“那把锁”
在 Android 17 之前,MessageQueue 内部几乎所有操作都依赖于一个私有的 Object 对象(通常就是 MessageQueue 实例本身)来作为同步锁:
Java
1 | // 传统实现(Android 17 之前) |
这把锁是一个独占锁(Exclusive Lock)。这意味着:如果后台线程正在向主线程发送消息(加锁),而此时主线程刚好执行完任务想去取下一条消息(也需要加锁),主线程就会被后台线程阻塞。这就是造成 UI 掉帧和“优先级反转”的典型场景。
2. Android 17 是如何实现“无锁”的?
Android 17 并没有简单地删掉 synchronized 关键字,而是改变了底层的数据结构,将原本的一个“优先级链表”拆分成了两个部分,并配合 CAS (Compare-And-Swap) 原子操作来实现并发安全。
核心架构:DeliQueue
- 入队端(Treiber Stack): 当任何线程调用
enqueueMessage时,消息不再直接插入优先级队列,而是压入一个无锁栈(Lock-free Stack)。这个过程使用 CAS 指令,不需要挂起线程,多线程并发时性能极高。 - 出队端(Min-heap): Looper 线程(通常是 UI 线程)拥有一个私有的最小堆(Min-heap)。由于这个堆只有 Looper 线程自己能访问,因此完全不需要锁。
- 同步机制: 当 Looper 发现堆里没消息了,它会一次性从无锁栈中取出所有新消息,排序后放入自己的最小堆。