Handler(二):MessageQueue 为什么不是普通队列?

0 阅读13分钟

Handler(二):MessageQueue 为什么不是普通队列?

上一篇讲清楚了一件事:

Handler.post 只是把 Runnable 包装成 Message,
然后放进目标 Looper 的 MessageQueue。

但新的问题马上来了:

Message 已经进队列了,为什么不是马上执行?
postDelayed 为什么不需要新开一个定时线程?
主线程没消息时为什么不会 while 空转烧 CPU?
为什么有些普通消息会被同步屏障挡住?
为什么 async 消息可以越过屏障?
quit / quitSafely 之后为什么 post 可能返回 false

这些问题说明:MessageQueue 不是普通 FIFO 队列。

它不只是“保存消息”,还负责:

时间判断:
    哪条消息到了 when,哪条消息还没到。
​
顺序选择:
    到期消息、同步屏障、异步消息之间如何选择。
​
空闲判断:
    队列暂时没有可执行消息时,是否执行 IdleHandler。
​
线程等待:
    没有可立即执行消息时调用 nativePollOnce;
    是否真正阻塞,取决于本轮 timeout。
​
线程唤醒:
    新消息改变当前等待结果时,
    必要时通过 nativeWake 叫醒正在等待的 Looper。
​
退出语义:
    quit / quitSafely 后,哪些消息还能处理,哪些 post 会失败。

所以这篇只回答一个问题:

Message 入队以后,MessageQueue 如何决定它什么时候能被 Looper 取出来?

本文源码以 Android 16 QPR2 的 android-16.0.0_r4 tag 为参考,正文只讨论 Handler 的稳定机制,不展开版本实现差异。

先把核心模型放出来:

MessageQueue
  |
  | 保存 Message
  | 按 when 判断是否到期
  | 遇到同步屏障时选择 async Message
  | 没有可执行消息时计算等待时间
  v
nativePollOnce
  |
  | timeout = -1 或正数:阻塞等待 fd / wake / timeout
  | timeout = 0:立即返回并重新检查队列
  v
再次进入 MessageQueue.next()

这套机制回答的不是“队列里有没有东西”,而是:

队列里现在有没有一条可以被取出来执行的 Message?

这两个问题不一样。队列里有消息,不代表现在就能执行。

一、Message 入队时带了哪些信息?

Message 不是一个只装 what 的小对象。Handler 把它放进 MessageQueue 前,至少有几类信息会影响后续选择:

字段作用对执行时机的影响
when这条消息最早可执行时间没到时间不能被取出
target == null内部同步屏障标记改变屏障后的消息选择规则
asynchronous是否异步消息可越过同步屏障

上一篇说过,callback 决定取出后的分发路径,Handler.enqueueMessage() 会把当前 Handler 写入 msg.target。这一篇只聚焦影响“何时能被选中”的 when、屏障和 asynchronous

最小链路:

Handler.post(r)
  -> sendMessageDelayed(msg, 0)
  -> sendMessageAtTime(msg, now)
  -> MessageQueue.enqueueMessage(msg, when)

如果是延迟任务:

handler.postDelayed(runnable, 5000);

它不会为这条任务单独创建定时线程。它会计算一个未来的 when,再把消息放进目标 MessageQueue

when = SystemClock.uptimeMillis() + 5000

后面 MessageQueue.next() 每次选择消息时,会检查候选消息的 when 是否已经到期。到期只表示“现在有资格被取出”,不表示一定在这个毫秒精确执行;前序回调、线程调度等仍可能让它更晚开始。

源码入口:

所以 postDelayed 的准确说法是:

postDelayed 是延迟调度接口,但不会为任务单独创建定时线程;
它计算未来的 when 并入队,达到相应时间后 Looper 才有机会取出它。

二、MessageQueue 是 FIFO 吗?

不能简单说是。

普通 FIFO 队列的规则是:

谁先入队,谁先出队。

MessageQueue 的规则更接近:

谁的 when 最早到期,谁更可能先被取出;
但如果遇到同步屏障,普通同步消息可能被挡住;
如果有 async 消息,它可能越过屏障先执行。

内部可以使用不同的数据结构,但对外必须维持同一组选择语义。与其把某一种链表实现当成全部真相,更适合先看这个语义骨架:

boolean enqueueMessage(Message msg, long when) {
    if (queueIsQuitting) {
        return false;
    }
​
    msg.when = when;
    saveMessageByExecutionTime(msg);
​
    if (newMessageChangesCurrentWaitResult) {
        nativeWake(mPtr);
    }
    return true;
}

这不是某个版本的可编译源码,而是四条稳定约束:

1. Message 带有最早可执行时间 when;
2. next() 要找到当前满足时间和屏障规则的候选消息;
3. 没有可执行消息时,要根据下一次机会计算等待时间;
4. 新消息改变当前等待结果时,要唤醒目标 Looper 线程。

因此,执行顺序不是简单复制调用 post() 的先后顺序。

举个例子:

handler.postDelayed(taskA, 10_000);
handler.post(taskB);

虽然 taskA 先入队,但 taskBwhen 更早,所以 taskB 会先被取出。

因此不要把 MessageQueue 理解成普通 FIFO:

它更像目标 Looper 背后的消息选择器。

三、MessageQueue.next() 为什么可能阻塞?

Looper.loop() 会不断调用 MessageQueue.next()

如果队列里有一条已经到期、且没有被屏障挡住的消息,next() 返回它。

如果没有可执行消息,next() 不能空转。否则主线程没消息时就会一直烧 CPU。

所以 next() 会计算下一次应该等多久:

队列没有消息:
    可以一直等,直到被唤醒。
​
队列有消息,但还没到 when:
    等到 when 对应的剩余时间。
​
遇到同步屏障,但没有 async 消息:
    等待后续消息或屏障移除。

逻辑骨架:

Message next() {
    int timeout = 0;
​
    for (;;) {
        nativePollOnce(ptr, timeout);
​
        long now = SystemClock.uptimeMillis();
        Message msg = selectExecutableMessage(now);
​
        if (msg != null) {
            removeFromQueue(msg);
            return msg;
        }
​
        timeout = computeNextPollTimeout(now);
        if (runIdleHandlersIfEligible()) {
            timeout = 0; // IdleHandler 可能又入队,下一轮先重新检查
        }
    }
}

这同样不是某个版本的可编译源码,只表示三个稳定动作:

1. 按上一轮算出的 timeout 进入 native 等待;
2. 醒来后按 when、屏障和 async 规则选择当前可执行 Message;
3. 没有可执行 Message 时,重新计算 timeout,再进入下一轮。

IdleHandler 是队列满足空闲条件时插入的回调机会。至于消息选择和状态更新由对象锁、并发容器还是其他同步方式保护,属于内部实现,不是这篇主线依赖的机制。

源码入口:

这里要强调:

Looper.loop 是循环,但不是空转。
​
没有可立即执行消息时,MessageQueue 会调用 nativePollOnce。
timeout 为正数或 -1 时,目标线程进入阻塞等待;
timeout 为 0 时,poll 立即返回并重新检查队列。

例如需要运行 IdleHandler 或快速重查队列时,本轮 timeout 可能是 0。所以不能把“调用 nativePollOnce”直接等同于“线程这次一定睡着”。

这就是“主线程没有消息时为什么不烧 CPU”的关键。

四、nativePollOnce 和 Java sleep 有什么区别?

nativePollOnce 不是简单的 Thread.sleep()

它背后接到 native Looper,可以等待多类事件:

按照 Java MessageQueue 计算出的 timeout 等待;
等待 wake event;
等待注册的文件描述符事件;
等待 native 层 callback;
等待超时返回。

源码入口:

通俗一点说:

Java 层 MessageQueue 负责算:
    现在有没有 Java Message 可以执行?
    如果没有,最多该等多久?
​
native Looper 负责等:
    按传入的 timeout 等时间;
    等 fd 事件;
    等 wake;
    等其他 native 事件。

这里的边界非常重要:native Looper 不读取 Message.when,也不知道“哪一条 Java Message 到期”。Java MessageQueue 先把选择结果换算成一个 timeout,native 层只负责按这个 timeout 等待或立即返回,并处理 wake、已注册 fd 和 native callback。

所以排障时看到线程在 nativePollOnce,不能直接说“卡住了”。它可能只是:

当前确实没有可执行消息;
或者下一条延迟消息还没到时间;
或者线程正在等 wake、已注册的输入或其他 fd 事件、以及 timeout

如果一条已到期消息的入队请求当时被接受,而且它改变了当前等待结果,正常入队路径会触发 nativeWake()。因此,线程栈停在 nativePollOnce 通常是“MessageQueue 当前认为没有可执行消息”的结果,不是消息迟到的独立根因。下一步应该回查:消息是否被队列接受、when 是否已到、是否存在有证据的屏障场景、消息是否已被移除,以及 Looper 是否正在退出。

五、其他线程入队后,为什么要 nativeWake?

假设主线程已经以正 timeout 或 -1 阻塞在 nativePollOnce,这时子线程投递一条会改变当前等待结果的消息:

mainHandler.post(() -> updateUi());

如果只是把消息放进队列,但不叫醒主线程,主线程可能还会继续睡到原来的超时时间。于是需要 nativeWake

nativeWake 的作用很容易被误解。

准确说法是:

nativeWake 不是执行消息。
nativeWake 只是叫醒正在等待的 Looper 线程。
醒来之后,Looper 还要重新进入 MessageQueue.next(),
判断现在是否有可执行 Message。

源码入口:

最小链路是:

子线程
  -> MessageQueue.enqueueMessage()
  -> 判断新消息是否改变当前等待结果
  -> 必要时 nativeWake()
​
目标线程
  -> nativePollOnce 被唤醒
  -> 回到 MessageQueue.next()
  -> 重新检查队列
  -> 取出可执行 Message

所以 nativeWake 解决的是“目标线程睡着了怎么叫醒”,不是“消息怎么执行”。

六、同步屏障为什么是 target == null 的 Message?

同步屏障是 MessageQueue 里最容易被讲歪的点。

普通消息都有 target,因为被取出来后要交给某个 Handler:

msg.target = handler

同步屏障是一条特殊消息,它没有目标 Handler:

msg.target == null

它的作用不是执行某段业务代码,而是改变队列选择规则:

屏障后面的同步消息暂时不能被取出;
异步消息可以越过屏障继续执行。

image.png

UI traversal 中一个典型的调用关系是:

ViewRootImpl.scheduleTraversals()
  -> MessageQueue.postSyncBarrier()
  -> Choreographer.postCallback(CALLBACK_TRAVERSAL, ...)
  -> 异步的帧调度路径可以越过屏障
  -> ViewRootImpl.doTraversal() 先移除屏障,再执行 traversal

所以不是“Choreographer 创建了屏障”,而是 ViewRootImpl 在安排 traversal 时插入屏障,再交给 Choreographer 驱动相应回调。

源码入口:

这张图的核心是:

同步屏障不是“更高优先级任务”。
它本身不会 dispatch。
它只是让 MessageQueue 在选择下一条消息时跳过普通同步消息,
去找后面的 async Message。

async message 也不要理解成“更快的消息”。准确说法是:

async message 是可以越过同步屏障的消息。

它不保证执行时间更短,也不让慢回调变快。它只改变“能不能在屏障存在时被选出来”。

源码入口:

这也是为什么不要随便把业务 Handler 改成 async。async 不是通用加速器,它会改变屏障下的顺序语义。同步屏障本身也是隐藏的系统机制,业务代码不应把它当作调优接口;系统内部使用时,token 必须沿取消、异常和正常完成路径成对移除。

七、IdleHandler 是低优先级任务队列吗?

不准确。

IdleHandler 会在队列进入“空闲”判断时被调用。所谓空闲,通常指:

当前没有可立即执行的 Message;
或者下一条 Message 还没到 when

这和“低优先级队列”不是一回事。IdleHandler 没有严格的调度保证,也不适合放重任务。它适合做轻量、可推迟、不会影响下一条消息及时性的工作。

错误用法:

把耗时 I/O 放进 IdleHandler;
以为 IdleHandler 一定马上执行;
以为 IdleHandler 可以稳定替代后台线程。

更稳的理解是:

IdleHandler 是 MessageQueue 临时空闲时给你的一个轻量回调机会。
它不是任务调度框架。

如果 IdleHandler 自己执行很久,同样会占住 Looper 线程,后面的消息还是会延迟。

八、quit / quitSafely 之后为什么 post 可能失败?

Handler 的投递方法很多都会返回 boolean

boolean ok = handler.post(runnable);

这个返回值不是装饰。true 表示这次入队请求在当时被队列接受,false 表示没有被接受。

MessageQueue 正在退出或已经退出时,入队可能失败:

Looper.quit():
    尽快退出,队列不再继续正常接收和处理后续消息。
​
Looper.quitSafely():
    处理请求退出时已经到期的消息,
    未来时间的延迟消息会被移除,不会继续等待执行。

所以排查“Handler 没执行”时,第一步不要直接看 handleMessage()。先问:

post/sendMessage 有没有发生?
返回值是不是 true?
目标 Looper 是否已经 quit?
HandlerThread 是否已经退出?

如果 post() 已经返回 false,后面再分析同步屏障、slow delivery、dispatch 慢都没有意义。这次入队请求已被拒绝,消息没有进入目标队列。

反过来,true 也不是“最终一定执行”的承诺。入队后,消息仍可能被 removeCallbacks()removeMessages()removeCallbacksAndMessages() 清掉,也可能在后续生命周期退出时被取消。

九、消息为什么还不能被取出?

先统一一条正常 Handler 消息的主链:

投递
  -> 入队
  -> 等待
  -> 取出
  -> 分发与执行

“退出”不是每条消息都会经历的最后阶段。下面这些情况会从旁路阻止消息继续沿主链执行:

根本没有投递;
入队请求被拒绝;
入队后被 remove;
生命周期结束后主动取消;
Looper / HandlerThread 退出。

排除这些旁路后,第二篇只关心“为什么还在等待、还没有被 next() 取出”:

when 未到:
    postDelayed、sendMessageAtTime 或重试退避把执行时间放在未来。
​
前序消息慢:
    目标 Looper 同一时间只能 dispatch 一条 Message。
​
线程调度延迟:
    目标线程已经 runnable,但迟迟没有获得 CPU。
​
有证据的同步屏障场景:
    普通同步消息被屏障挡住,异步消息可以越过。
​
当前没有可执行消息:
    MessageQueue 调用 nativePollOnce;
    timeout 可能让线程阻塞,也可能为 0 并立即返回重查队列。

至于消息已经开始 dispatch 后,回调内部是否卡在 Binder、锁、I/O 或 native/HAL,属于第四篇的执行证据,不在这里提前展开。

十、模糊说法 vs 准确说法

第一组:

模糊说法:
    MessageQueue 是队列。
​
准确说法:
    MessageQueue 是带时间、屏障、空闲、退出和 native 等待唤醒能力的消息选择器。

第二组:

模糊说法:
    postDelayed 是开定时器。
​
准确说法:
    postDelayed 是延迟调度接口,但不会为每条任务单独创建定时线程;
    它计算未来的 Message.when 并入队,到期后消息才有机会被 Looper 取出;
    前序回调和线程调度仍可能让它更晚开始。

第三组:

模糊说法:
    没消息时主线程在死循环。
​
准确说法:
    Looper.loop 是循环,但不是空转;
    没有可立即执行消息时会调用 nativePollOnce;
    timeout 为正数或 -1 时阻塞,timeout 为 0 时立即返回。

第四组:

模糊说法:
    nativeWake 执行消息。
​
准确说法:
    nativeWake 只是唤醒目标 Looper 线程;
    醒来后还要重新进入 MessageQueue.next 判断是否有可执行消息。

第五组:

模糊说法:
    async 消息更快。
​
准确说法:
    async 消息不是更快的普通消息;
    它是可以越过同步屏障的特殊消息。

十一、这一篇的结论

MessageQueue 不是普通 FIFO 队列。

它真正回答的是:

一条 Message 已经入队之后,
为什么现在能执行,
或者为什么现在还不能执行。

消息没执行,不能只说“队列堵了”。先确认它是否还在正常主链,再判断是不是从旁路被拒绝、移除、取消或因 Looper 退出而中断。

如果入队请求当时被接受,并且有证据表明消息尚未被移除、仍在等待,第二篇真正要检查的是:

when 未到;
前序消息执行太久;
线程调度延迟;
有证据的同步屏障场景;
当前没有可执行消息而调用 nativePollOnce。

到这里已经解决了两件事:

第一篇:
    任务怎么从调用线程进入目标线程。
​
第二篇:
    任务进入队列以后,为什么不是马上执行。

下一篇继续往系统里放:

主线程、HandlerThread、Binder 线程到底怎么区分?

延伸阅读