手把手扒光 Android DeliQueue 底裤

0 阅读12分钟

MessageQueue 现状

  1. 全局锁竞争与优先级反转 (Priority Inversion)

    • 现状:主线程(Looper)拿消息、子线程发消息、或者删消息时,全都要抢同一把锁。
    • 劣势:如果某个低优先级的后台网络线程正在疯狂发消息(占用了锁),此时主线程正好收到了底层极其高优的 VSYNC 信号准备进行 UI 绘制(doFrame),主线程会被迫在锁外排队等待。这直接导致了帧率截止时间的错失,引发明显的界面卡顿(Jank)。
  2. O(N) 的插入与遍历损耗

    • 现状:当发来一个指定执行时间(when)的消息时,系统必须加着锁,从头开始遍历单向链表,直到找到合适的位置插入。
    • 劣势:当队列中积压了大量消息时,每次插入的时间复杂度都是 O(N)。且在遍历期间,整个系统处于锁死状态。
  3. 丑陋且低效的同步屏障 (SyncBarrier)

    • 现状:为了让 UI 绘制消息优先执行,旧版会在链表头插一个 target 为 null 的“屏障节点”。
    • 劣势:主线程在循环时,一旦看到屏障,就必须在链表里使用 while 循环不断往后找,直到找到第一个标记为异步的消息。这种寻找也是 O(N) 复杂度的,拖慢了高优消息的调度速度。
  4. 危险且复杂的移除操作 (removeMessages)

    • 现状:为了从队列中删掉特定的消息,必须加锁遍历整个链表,找到后还要小心翼翼地修改前后节点的指针使其断开。高并发下极其容易出错或死锁。

二、数据结构解析

DeliQueue/MessageQueue.java

源码:cs.android.com/android/pla…

public final class MessageQueue {
	private static final String TAG = "DeliQueue";
  private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/
  ...
  ...
  MessageStack mStack = new MessageStack();
}

image.png

MessageStack.java

源码:frameworks/base/core/java/android/os/MessageStack.java​​

public final class MessageStack {
    private static final String TAG = "MessageStack";

    private static final VarHandle sTop;
    private volatile Message mTopValue = null;

    private static final VarHandle sFreelistHead;
    private volatile Message mFreelistHeadValue = null;

    // The underlying min-heaps that are used for ordering Messages.
    private final MessageHeap mSyncHeap = new MessageHeap();
    private final MessageHeap mAsyncHeap = new MessageHeap();

    // This points to the most-recently processed message. Comparison with mTopValue will indicate
    // whether some messages still need to be processed. This value excludes the quitting sentinel.
    private Message mLooperProcessed = null;
    ...
    ...
}

image.png

mSyncHeap mAsyncHeap (双堆架构)

在旧版的 LegacyMessageQueue 中,为了保证界面的高优先级渲染,Google 发明了“同步屏障(SyncBarrier)”:一旦开启屏障,只有打上了 setAsynchronous(true) 标记的异步消息(通常是 ChoreographerdoFrame 等界面绘制消息)才能被优先执行,普通同步消息会被阻塞。这种逻辑在单向链表中需要用大段的 while 循环去判断,极易导致死循环和锁竞争。

在新的架构中,Google 用“结构设计”替代了“算法逻辑”:

  • mSyncHeap (同步消息最小堆) :主线程维护的用于存放所有普通/同步消息的优先级队列(按 when 排序)。
  • mAsyncHeap (异步消息最小堆) :专门用于存放通过 setAsynchronous(true) 标记的高优先级异步消息。

当主线程在 MessageQueue.next() 中挑选下一个要执行的消息时,它不再需要去判断什么“同步屏障标记节点”了。它只需要做极其简单的数据结构判决:

  1. 检查是否存在同步屏障(现在只是一个标志位)。
  2. 如果存在,直接去 mAsyncHeap 的堆顶拿消息,完全忽略 mSyncHeap
  3. 这种双堆隔离设计,让主线程寻找最高优渲染消息的时间复杂度降到了 O(1),极其高效。

image.png

mTopValue (无锁状态核心标识)

mTopValue 实际上指向了并发栈 mStack栈顶元素(Top Node)

  • 它就是 Google 提到的 Treiber Stack(无锁并发栈) 的最核心指针。所有多线程高并发调用 sendMessage 时,底层执行的 CAS(Compare-And-Swap) 原子操作,抢夺和替换的就是这个 mTopValue 的引用。
  • 只要一个子线程成功把自己的 Message 挂到了 mTopValue 上,并且把原先的 mTopValue 设为了自己的 next,压栈就成功了。
  • 它的类型是 Message(它就是一个消息链的起点,只不过这个链是临时的,随时会被主线程一把抓走)。

image.png

mLooperProcessed (排空游标 / 水位线)

在传统的队列中,如果你要把一个消息从“接收区”移到“工作台”,你需要做物理删除(把节点的指针断开),这必不可免地需要加全局锁,否则子线程刚好在这个时候插消息,链表就断了。

但在 DeliQueue 中,为了绝对的无锁,主线程(Looper)绝对不会去修改/断开并发栈上的链表结构! 它只做只读遍历。

既然不断开链表,那主线程怎么知道哪些消息是“新来的”,哪些消息是“上一轮已经拿去 Heap 里的”呢?

答案就是 mLooperProcessed (Looper 已处理游标) 。它就是一个“书签”,记录了主线程上一次扫描到了哪里。

image.png

三、场景逻辑推演

消息生产阶段

我们假设这 4 个消息中有异步的绘制消息普通的同步消息

  • Msg1 (普通同步消息)
  • Msg2 (异步渲染消息,带有 setAsynchronous(true))
  • Msg3 (普通同步消息)
  • Msg4 (异步紧急消息,带有 setAsynchronous(true))

以下是加入了双堆(Dual-Heap)变化的完整推演过程:

初始状态:系统刚启动

  • 入口与书签

    • mTopValue = null (无锁并发栈为空)
    • mLooperProcessed = null (主线程还没看过任何消息)
  • 消费者工作台 (双堆)

    • mSyncHeap = [] (普通消息堆为空)
    • mAsyncHeap = [] (异步消息堆为空)

第一轮:子线程发消息 (高并发入栈)

  1. 子线程 A 发送 Msg1 (普通消息):

    • 利用 CAS,让 Msg1.next = null,然后更新 mTopValue = Msg1
  2. 子线程 B 发送 Msg2 (异步消息):

    • 利用 CAS,让 Msg2.next = Msg1,然后更新 mTopValue = Msg2

📝 此时的数据结构状态:

  • mStack 结构: mTopValue -> [Msg2(异)] -> [Msg1(同)] -> null
  • mSyncHeap : [] (未搬运)
  • mAsyncHeap : [] (未搬运)

第二轮:主线程 (Looper) 醒来,开始搬运与分拣 (heapSweep)

主线程进入 MessageQueue.next(),立刻执行 heapSweep

  1. 确定区间: 主线程看到当前 mTopValueMsg2,自己的书签 mLooperProcessednull

  2. 顺藤摸瓜并分拣 (从 Msg2 开始向下遍历,遇到 null 停止):

    • 抓取 [Msg2(异)],发现它带有异步标记,执行 SiftUp 插入 mAsyncHeap
    • 往下看,抓取 [Msg1(同)],发现它是普通同步消息,执行 SiftUp 插入 mSyncHeap
    • 再往下看,遇到 null,等于书签 mLooperProcessed,立刻停止扫描
  3. 更新书签: 主线程把刚刚扫描起点的 Msg2 记作新书签:mLooperProcessed = Msg2

📝 此时的数据结构状态:

  • mStack 结构: mTopValue -> [Msg2(异)] -> [Msg1(同)] -> null (主线程不修改指针,保留原状)
  • mSyncHeap : [ Msg1 ] (按 when 排序)
  • mAsyncHeap : [ Msg2 ] (按 when 排序)

随后,主线程根据是否有 SyncBarrier,从相应的 Heap 堆顶取出消息执行。


第三轮:子线程又发新消息

在主线程执行上面消息的过程中,其他线程又发来了新消息:

  1. 子线程 C 发送 Msg3 (普通消息):

    • 利用 CAS,让 Msg3.next = Msg2,然后更新 mTopValue = Msg3
  2. 子线程 D 发送 Msg4 (异步消息):

    • 利用 CAS,让 Msg4.next = Msg3,然后更新 mTopValue = Msg4

📝 此时的数据结构状态:

  • mStack 结构: mTopValue -> [Msg4(异)] -> [Msg3(同)] -> [Msg2(异)] -> [Msg1(同)] -> null
  • (假设 Heap 里的消息由于未到触发时间,还暂存在工作台上)

第四轮:主线程 (Looper) 再次醒来搬运

主线程执行完手头的活,再次进入 MessageQueue.next() 执行 heapSweep

  1. 确定区间: 主线程看到最新的 mTopValue 变成了 Msg4。书签 mLooperProcessedMsg2

  2. 顺藤摸瓜并分拣 (从 Msg4 开始向下遍历,遇到 Msg2 停止):

    • 抓取 [Msg4(异)],它是异步消息,执行 SiftUp 插入 mAsyncHeap(此时 mAsyncHeap 会根据 Msg4 Msg2 的触发时间重新排序)
    • 往下看,抓取 [Msg3(同)],它是普通同步消息,执行 SiftUp 插入 mSyncHeap(此时 mSyncHeap 会根据 Msg3 Msg1 的时间排序)
    • 再往下看,发现是 [Msg2(异)]
    • 拦截生效Msg2 等于书签 mLooperProcessed,主线程知道:Msg2 和它后面的 Msg1,我上一轮已经分拣过了,立即停止扫描
  3. 更新书签: 主线程把最新的起点 Msg4 记作新书签:mLooperProcessed = Msg4

📝 最终的数据结构状态:

  • mStack 结构:依然保持那一串长长的链表 (旧节点会随着内存回收机制逐步被清理,我们这里不深究释放过程)。
  • mSyncHeap : [ Msg1, Msg3 ] (内部已按 when 的时间先后排好序)
  • mAsyncHeap : [ Msg2, Msg4 ] (内部已按 when 的时间先后排好序)

核心变化

加上双堆变化后,能明显感觉到这套架构的强悍之处: 多线程把无序的、什么类型都有的消息“一锅粥”地全倒在 mTopValue 上。而主线程就像一个极其熟练的图书管理员,通过 mLooperProcessed 游标精准地拿到新书,然后有条不紊地把它们分别塞进“同步书架(mSyncHeap)”和“异步书架(mAsyncHeap)”。

当 UI 渲染的信号(VSYNC)到来需要开启同步屏障时,主线程连看都不看同步书架一眼,直接去异步书架拿书,没有丝毫的迟疑与遍历的损耗!

消息消费阶段

📝 前情提要,此时主线程的工作台状态:

  • mSyncHeap : [ Msg1(同), Msg3(同) ] (内部已按 when 时间先后排好序,假设 Msg1 最早)
  • mAsyncHeap : [ Msg2(异), Msg4(异) ] (内部已按 when 时间先后排好序,假设 Msg2 最早)

第五轮:消费阶段 - 出堆与决策 (The Poll Phase)

主线程收割完新消息后,开始从工作台拿取消息执行。

场景 1:当前没有同步屏障 (Normal Mode)
  1. 对比堆顶:主线程看到桌子上两堆书。它取出 mSyncHeap 的最顶层(最早)消息 Msg1,和 mAsyncHeap 的最顶层消息 Msg2

  2. 公平竞争:比较 Msg1.whenMsg2.when,谁的时间更小(谁该先执行)就选谁。假设 Msg1 时间更早。

  3. 弹出堆顶:从 mSyncHeap 弹出 Msg1

  4. 墓碑与时间校验

    • 检查 Msg1 是否被标记为墓碑(Tombstone,即被调用了 removeMessages 取消)?

      image.png

    • 如果没有被取消,检查 Msg1.when 是否小于等于当前时间?

  5. 返回执行:如果一切正常,主线程的 MessageQueue.next()Msg1 递交给 Looper,Looper 随后调用 Msg1.target.dispatchMessage() 执行代码。

📝 消费后状态: mSyncHeap : [ Msg3(同) ] mAsyncHeap : [ Msg2(异), Msg4(异) ]

场景 2:当前开启了同步屏障 (SyncBarrier Mode)

前提:此时底层收到了 VSYNC 信号,即将要开始进行极其重要的 UI 绘制,为了保证不掉帧,系统向消息队列插入了一个“同步屏障”节点(或设置了屏障标志)。

  1. 直接抄近道:主线程发现有同步屏障!它直接无视 mSyncHeap(哪怕里面的 Msg3 其实早就过期了该执行了)。

  2. 单推异步堆:主线程只盯着 mAsyncHeap 看,取出最顶层的 Msg2

  3. 弹出堆顶:从 mAsyncHeap 弹出 Msg2

  4. 墓碑与时间校验

    • 检查 Msg2 是不是墓碑?不是。
    • 检查 Msg2.when 到时间了吗?到了。
  5. 返回执行:主线程将 Msg2 递交给 Looper,去执行极其关键的界面渲染工作。

📝 消费后状态: mSyncHeap : [ Msg3(同) ] (继续被阻塞), mAsyncHeap : [ Msg4(异) ]


第六轮:消费阶段 - 无事可做与休眠 (The Park Phase)

假如主线程飞速执行完了 Msg1Msg2,现在又回来 MessageQueue.next() 找事情做。 (假设此时没有新消息发来, mLooperProcessed 也没有变化)

  1. 对比堆顶:主线程对比剩下的 Msg3Msg4

  2. 发现未到期:假设虽然选出了 Msg3,但是它规定的执行时间是在 2 秒后

  3. 计算时差与休眠 (epoll_wait)

    • 主线程更新自身状态标志为 STACK_NODE_TIMEDPARK(定时休眠)。
    • 计算距离 Msg3.when 还有 2 秒,调用底层 nativePollOnce(ptr, 2000) 进入休眠,交出 CPU 控制权。

第七轮:消费阶段 - 墓碑机制发威 (Tombstone Deletion)

这里补充一个在旧版极难处理,而在 DeliQueue 中极为优雅的场景:取消消息

假设在上面的休眠期间,由于用户退出了当前页面,业务代码调用了 handler.removeMessages(Msg4.what)

  1. 标记墓碑 (惰性删除) : 系统不会去遍历 mAsyncHeap 找到 Msg4 然后把它删掉(因为这会破坏堆结构,且极其低效)。系统只会在底层的数据标记上,将匹配的 Msg4 设为 Tombstone(墓碑/已取消)
  2. 休眠结束,醒来干活: 时间到了,主线程醒来。假设此时终于轮到 Msg4mAsyncHeap 的最顶端了。
  3. 弹出并校验:主线程弹出 Msg4。在进入执行前的“墓碑校验”环节,发现:哦?这个 Msg4 已经被立了墓碑。
  4. 直接丢弃:主线程二话不说,直接把 Msg4 当垃圾丢进内存回收池(Freelist),不将它递交给 Looper。
  5. 继续循环:主线程重新去对比如今堆顶的消息,寻找下一个合法受害者。

核心变化

看完整个消费阶段,可以发现 DeliQueue 最大的两点颠覆:

  1. 同步屏障被“物理结构化”了:过去是在一条链表里用 while 循环苦苦寻找带异步标记的消息(O(N)),现在是两个完全分离的书架,找异步消息就是直接拿最上层那本(O(1))。
  2. 删除操作被“惰性化”了:过去为了删一个消息需要遍历链表、断开前后指针,还得加锁防并发。现在仅仅是贴个“墓碑”标签,等这个消息浮出水面准备执行时,直接像弹飞灰尘一样弹掉。

四、流程图解析

graph TD
    classDef stack fill:#fff3e0,stroke:#f57c00,stroke-width:2px,color:#000000;
    classDef process fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#000000;
    classDef heap fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#000000;
    classDef loop fill:#f3e5f5,stroke:#8e24aa,stroke-width:2px,color:#000000;
    classDef decision fill:#e8eaf6,stroke:#3949ab,stroke-width:2px,color:#000000;
    
    %% 主入口
    Start(("主线程 Looper 调用 MessageQueue.next")):::loop --> CallSweep

    %% heapSweep 流程
    subgraph HeapSweep ["🧹 heapSweep 转移过程"]
        CallSweep["执行 mStack.heapSweep()"]:::process
        
        ReadTop["读取当前 mTopValue 指针(获取此刻最新栈顶)"]:::process
        CallSweep --> ReadTop
        
        SetCurrent["设置游标: current = mTopValue"]:::process
        ReadTop --> SetCurrent
        
        CheckEnd{"current == mLooperProcessed? 或者 current == null?"}:::decision
        SetCurrent --> CheckEnd
        
        CheckEnd -- "No (发现未处理的新消息)" --> ProcessNode["处理 current 节点"]:::process
        
        ProcessNode --> CheckAsync{"current.isAsynchronous()"}:::decision
        
        CheckAsync -- "Yes (是异步高优消息)" --> SiftAsync["SiftUp 插入排序到 mAsyncHeap"]:::heap
        CheckAsync -- "No (是普通同步消息)" --> SiftSync["SiftUp 插入排序到 mSyncHeap"]:::heap
        
        SiftAsync --> NextNode
        SiftSync --> NextNode
        
        NextNode["current = current.next (沿着单向链表往下看)"]:::process
        NextNode --> CheckEnd
        
        CheckEnd -- "Yes (扫到水位线了,增量收割完毕)" --> FinishSweep["更新水位线: mLooperProcessed = mTopValue"]:::process
    end

    %% 出堆消费流程
    subgraph HeapConsume ["双堆消费过程(单线程安全)"]
        FinishSweep --> CheckBarrier{"当前存在 SyncBarrier (同步屏障) 吗?"}:::decision
        
        CheckBarrier -- "Yes (拦截普通消息)" --> PollAsync["只从 mAsyncHeap 获取堆顶 (最早) 消息"]:::heap
        
        CheckBarrier -- "No (公平竞争)" --> CompareHeaps["对比 mAsyncHeap 和 mSyncHeap 堆顶的 when 时间"]:::heap
        CompareHeaps --> PollWinner["弹出两者中 when 最小的那个消息"]:::heap
        
        PollAsync --> PostProcess
        PollWinner --> PostProcess
        
        PostProcess["判断时间、过滤 Tombstone 最终返回给 Looper 执行"]:::loop
    end

五、时间复杂度

旧版 MessageQueueDeliQueue
插入O(N)O(1)
移除O(1)O(logN)
锁竞争