Skip to content

消息延迟边界

从 postDelayed 到 MessageQueue.next,解释 uptimeMillis、消息到期、线程繁忙和取消竞态如何共同决定实际执行时刻。

基于android-17.0.0_r1
AndroidHandlerMessageQueueSystemClock源码阅读

消息延迟边界 ​

Handler.postDelayed(r, delayMillis) 的参数叫“延迟”,但源码真正保存的是一个绝对的 Message.when。这个时间点只决定消息何时具备出队资格;消息何时真正调用 Runnable.run(),还 取决于目标 Looper 何时回到 MessageQueue.next()、队列前面是否有更早消息,以及当前分发是否 已经结束。

本文面向已经读过 Handler消息发送、 MessageQueue消息入队、 MessageQueue取消息 和 Looper主循环 的读者。文章只回答一个问题:从调用 postDelayed 到回调开始,时间戳、队列和线程分别做了什么;不讨论 AlarmManager 的系统唤醒 协议,也不把没有固定上界的调度误写成精确计时器。

读完后,读者应能从 Runnable 反查它的 Message.when,解释设备深度睡眠为什么会改变相对墙钟 时间,判断消息是“尚未到期”“已经到期但尚未取出”还是“已经取出等待分发”,并定位 removeCallbacks 为什么可能赶不上已经取出的消息。

1. 时间语义 ​

源码文件:frameworks/base/core/java/android/os/SystemClock.java

Android 暴露的三个时钟不能互换。currentTimeMillis() 是可以被用户或网络调整的墙钟; uptimeMillis() 从开机计时但在深度睡眠时停止;elapsedRealtime() 包含深度睡眠。Handler 的 延迟 API 选择的是 uptimeMillis(),因此它表达的是“目标线程的 uptime 时间到达”,不是“墙上 时间经过了多少毫秒”。

java
/*
 * uptimeMillis is counted in milliseconds since the system was booted.
 * This clock stops when the system enters deep sleep ...
 * elapsedRealtime ... include deep sleep.
 */
public static native long uptimeMillis();
public static native long elapsedRealtime();

uptimeMillis() 的单调性适合队列排序:调用方的墙钟被调整,不会让一个已经排好的消息突然 变成“早于”或“晚于”另一个消息。代价是深度睡眠期间这个时钟不前进,消息的 when 也不会因为 设备睡眠而自动消耗掉剩余时间。

时钟深度睡眠期间适合回答的问题Handler 是否使用
currentTimeMillis()继续走,可能被调整现在是哪个日期和时间否
uptimeMillis()停止CPU 活跃时间过去多久是
elapsedRealtime()继续走设备实际开机经过多久否

这张表不是三个 API 的完整说明,而是解释延迟结果时必须先消除的术语歧义。若需求是“现实时间 到点”,不能从 postDelayed 的 uptimeMillis 语义推导出该保证。

2. 相对转换 ​

源码文件:frameworks/base/core/java/android/os/Handler.java

postDelayed 不直接操作 Runnable 的时间。它先把 Runnable 放进 Message.callback,再调用 sendMessageDelayed;后者把负数归零,并在一次读取的 uptimeMillis() 上加上相对延迟。

java
public final boolean postDelayed(@NonNull Runnable r, long delayMillis) {
    return sendMessageDelayed(getPostMessage(r), delayMillis);
}

public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
    if (delayMillis < 0) {
        delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}

因此 postDelayed(r, 1000) 的源码含义可以写成:

text
enqueue.when = uptimeMillis() 在投递瞬间的读数 + 1000

它不等价于“在调用线程睡眠 1000 毫秒”,也不等价于“回调线程在 1000 毫秒后一定空闲”。 调用方读取时钟、创建 Message、获得队列锁和完成入队都会发生在这次读取之后;这些时间会被 包含在从调用返回到目标 when 的剩余预算中。

2.1 绝对入口 ​

源码文件:frameworks/base/core/java/android/os/Handler.java

postAtTime 和 sendMessageAtTime 接受已经计算好的绝对 uptimeMillis。它们不再重新读取 当前时间,所以同一个绝对时间可以让多个消息共享一个排序基准。

java
public final boolean postAtTime(@NonNull Runnable r, long uptimeMillis) {
    return sendMessageAtTime(getPostMessage(r), uptimeMillis);
}

public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    if (queue == null) {
        RuntimeException e = new RuntimeException(
                this + " sendMessageAtTime() called with no mQueue");
        Log.w("Looper", e.getMessage(), e);
        return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);
}

这也解释了两个失败边界:没有绑定队列的 Handler 返回 false;已经退出的队列则在更深的 enqueueMessage 中拒绝。无论哪一种,消息都不会获得一个“稍后再试”的时间。

3. 时间排序 ​

源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java

队列把 when 写入 Message,再按升序插入单链表。when == 0 是队头插入的特殊值;普通 延迟消息则按时间寻找第一个更晚节点。相同时间的消息不会因为排序算法而获得一个新的时间戳。

java
boolean enqueueMessage(Message msg, long when) {
    if (msg.target == null) {
        throw new IllegalArgumentException("Message must have a target.");
    }

    synchronized (this) {
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }
        if (mQuitting) {
            IllegalStateException e = new IllegalStateException(
                    msg.target + " sending message to a Handler on a dead thread");
            Log.w(TAG, e.getMessage(), e);
            msg.recycle();
            return false;
        }

        msg.markInUse();
        msg.when = when;
        Message p = mMessages;
        if (p == null || when == 0 || when < p.when) {
            msg.next = p;
            mMessages = msg;
        } else {
            Message prev;
            for (;;) {
                prev = p;
                p = p.next;
                if (p == null || when < p.when) {
                    break;
                }
            }
            msg.next = p;
            prev.next = msg;
        }
    }
    return true;
}

这里的锁保证的是“写入和排序的一致性”,不是“到期后立即执行”。入队返回 true 时, Message.when 已经固定,消息也已经属于队列;但目标线程可能仍在执行旧消息,或者尚未被 native poll 唤醒。

3.1 唤醒条件 ​

源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java

把新消息插入队头时,如果 Looper 正在阻塞,队列需要唤醒它重新计算超时。插入中部或尾部的 消息通常不需要唤醒,因为队头仍然决定下一次到期时刻;同步屏障和异步消息会改变这个判断。

java
boolean needWake;
if (p == null || when == 0 || when < p.when) {
    msg.next = p;
    mMessages = msg;
    needWake = mBlocked;
    if (p == null) {
        mLast = mMessages;
    }
} else {
    needWake = mBlocked && p.target == null && msg.isAsynchronous();
    // 插入中部或尾部,并继续维护 mLast 与异步消息计数
}

if (needWake) {
    nativeWake(mPtr);
}

这意味着“投递一个更晚的延迟消息”通常不会打断当前等待;“投递一个新的更早队头消息”才需要 让阻塞中的 next() 重新计算等待时长。唤醒动作只改变检查时机,不直接取出消息。

图中 nativeWake 是“重新检查”的边,不是“执行 Runnable”的边。真正的执行仍要经过 next() 出队和后续 Handler 分发。

4. 到期判断 ​

源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java

next() 先进入 native poll,再在 Java 锁内读取新的 uptimeMillis()。如果队头消息的 when 仍在未来,队列把差值转换成下一次 poll 的毫秒超时;只有 now >= msg.when 时,Message 才会 从链表摘下并返回给 Looper。

java
int nextPollTimeoutMillis = 0;
for (;;) {
    nativePollOnce(ptr, nextPollTimeoutMillis);

    synchronized (this) {
        final long now = SystemClock.uptimeMillis();
        Message msg = mMessages;
        if (msg != null && msg.target == null) {
            do {
                prevMsg = msg;
                msg = msg.next;
            } while (msg != null && !msg.isAsynchronous());
        }
        if (msg != null) {
            if (now < msg.when) {
                nextPollTimeoutMillis = (int) Math.min(
                        msg.when - now, Integer.MAX_VALUE);
            } else {
                // 摘除 msg,标记 in-use,并返回给 Looper
                return msg;
            }
        } else {
            nextPollTimeoutMillis = -1;
        }
    }
}

这里至少有三个时间点:

时间点源码事件消息状态
T0Handler 读取 uptimeMillis() 并入队when = T0 + delay
T1next() 读取 now 且 now >= when消息具备出队资格并被摘除
T2Handler.dispatchMessage() 调用 callbackRunnable.run() 开始

T1 不早于 when,但 T2 还要等 Looper 从 next() 返回并进入分发。文章讨论的“实际延迟” 如果只记录 T0 和 T2,就会把队列等待、线程忙碌和分发前开销混在一起。

5. 轮询超时 ​

源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java

当 next() 发现消息尚未到期,nextPollTimeoutMillis 会成为下一轮 nativePollOnce 的输入。这个值的作用是让 native 层在预计到期时返回;它不是一个会强制 Java 线程停止当前 callback 的抢占定时器。

java
if (now < msg.when) {
    nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
    mBlocked = false;
    // 摘除并返回消息
}

若当前线程已经在 Handler.dispatchMessage() 中运行一个耗时 callback,next() 根本不会被 调用,新的延迟消息也没有机会被取出。即使 native poll 在目标时间返回,Java 分发线程也必须 先结束当前 callback,才能重新检查队列。

源码文件:frameworks/base/core/java/android/os/Looper.java

java
Message msg = me.mQueue.next(); // might block
if (msg == null) {
    return false;
}

final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
msg.target.dispatchMessage(msg);

这段顺序说明了延迟消息不会打断当前消息:next() 一次只交出一个 Message, dispatchMessage() 返回后外层循环才会再次进入 next()。所以源码能支持的稳健表述是 “不早于到期时间才具备出队资格”;不能从 sendMessageDelayed 的返回值推出固定的最大晚点时间。

6. 深度睡眠 ​

源码文件:frameworks/base/core/java/android/os/SystemClock.java

SystemClock 的类注释明确说明,uptimeMillis() 在深度睡眠时停止,而 elapsedRealtime() 包含深度睡眠。Handler 使用前者,所以设备睡眠期间,消息的 uptime 预算不会继续减少。

java
/**
 * uptimeMillis is counted in milliseconds since the system was booted.
 * This clock stops when the system enters deep sleep.
 */
public static native long uptimeMillis();

/**
 * elapsedRealtime returns the time since boot, including deep sleep.
 */
public static native long elapsedRealtime();

假设投递时 uptimeMillis() == 10_000,延迟为 5_000,那么 when == 15_000。如果设备随后 进入深度睡眠,墙钟可能前进了很久,但 uptime 仍停在接近 10_000;设备恢复后,队列仍按 uptimeMillis() 继续等待到 15_000。这不是“消息丢失”,而是 API 选择的时钟语义。

因此,以下需求不能混用:

  • “线程保持活跃后再过 5 秒回调”:可以使用 Handler.postDelayed;
  • “无论设备睡眠与否,现实时间 5 秒后触发”:不能由本文这条 Handler 路径保证;
  • “进程被杀后仍要保留调度”:也不是 MessageQueue 的生命周期能力。

7. 分发滞后 ​

源码文件:frameworks/base/core/java/android/os/Looper.java

Looper 在取出 Message 后才记录分发开始时间,随后调用目标 Handler。若前一个 callback 占用 线程,后一个延迟消息的 when 可能早已过去,但它只能排在当前 callback 返回之后。

java
final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
msg.target.dispatchMessage(msg);
final long dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;

可以用三个差值定位晚点来源:

text
投递预算:when - T0 = delayMillis
队列晚点:T1 - when       (到期后仍未被 next() 取出)
分发前后:T2 - T1          (出队到 Runnable 开始的路径)
总观测:  T2 - T0

其中 T1 - when 最容易被前一个长 callback 放大;它不是 uptimeMillis 的精度误差。源码没有 给出一个脱离线程负载、队列状态和设备运行状态的统一误差常数,文章也不为不同设备虚构平均值。

图中 T0、T1、T2 不是框架字段,而是阅读源码时用于分解一次观测的三个标记。只有把 when、next() 和 dispatchMessage() 分开,才能判断问题发生在计时、队列还是线程负载。

8. 取消窗口 ​

源码文件:frameworks/base/core/java/android/os/Handler.java

removeCallbacks 最终按 Handler、Runnable 和 token 在队列中查找并移除尚未出队的消息。它不 能撤回已经由 next() 摘下、但尚未开始 dispatchMessage() 的 Message。

java
public final void removeCallbacks(@NonNull Runnable r) {
    mQueue.removeMessages(this, r, null);
}

源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java

next() 摘下消息时会把它从 mMessages 链表断开;之后另一个线程调用移除接口,已经不再能从 队列链表找到它。

java
msg.next = null;
msg.markInUse();
return msg;

因此取消存在明确的竞态窗口:

取消时机removeCallbacks 能否找到消息结果
when 尚未到,消息仍在链表通常可以消息被移除并回收
next() 已摘除,尚未分发不可以callback 仍可能运行
Runnable.run() 已开始不可以只能由 Runnable 自己检查取消标志

如果业务要求“取消后即使消息已出队也不执行”,需要在 Runnable 内再检查一个由业务拥有的 volatile/原子状态;removeCallbacks 本身只负责队列中尚未出队的部分。

9. 队列退出 ​

源码文件:frameworks/base/core/java/android/os/Handler.java

postDelayed 返回 true 只表示消息成功放入队列;Handler 的 Javadoc 明确保留了另一条路径: 如果 Looper 在消息交付时间之前退出,消息仍会被丢弃。

java
/**
 * @return true if successfully placed into the message queue.
 * A result of true does not mean the Runnable will be processed --
 * if the looper is quit before the delivery time ... the message will be dropped.
 */
public final boolean postDelayed(@NonNull Runnable r, long delayMillis) {
    return sendMessageDelayed(getPostMessage(r), delayMillis);
}

源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java

普通退出会清理待处理消息;next() 观察到 mQuitting 后销毁 native 资源并返回 null,外层 Looper 随即结束。安全退出的保留规则也由队列实现决定,不能从 Handler 的 postDelayed API 本身推导出“延迟消息一定执行”。

java
if (mQuitting) {
    dispose();
    return null;
}

这条路径与时间精度无关:消息可能还没有到期,也可能已经到期但尚未被取出;只要队列在交付前 结束,调用者都不应把此前的 true 当作完成确认。

10. 测试输入 ​

源码文件:frameworks/base/core/tests/coretests/src/android/os/MessageQueueTest.java

Android 17 的 MessageQueueTest 使用 HandlerThread、TestLooperManager 和固定的未来 时间戳测试 Legacy 队列的排序与清理。测试不是对所有设备延迟误差的统计,而是对队列不变量的 反向输入。

java
final long future = SystemClock.uptimeMillis() + LONG_DELAY_MS;
handler.sendEmptyMessageAtTime(0, future);
handler.sendEmptyMessageAtTime(1, future + 1);
handler.sendEmptyMessageAtTime(4, future + 4);
handler.sendEmptyMessageAtTime(3, future + 3);
handler.sendEmptyMessageAtTime(2, future + 2);
Message m = queue.peekLastMessageForTest();
assertEquals(m.what, 4);

输入先按乱序时间投递 0、1、4、3、2,再检查队尾;它证明入队结构维护了时间顺序,不证明 消息在 future 的墙钟时刻执行。

同一测试还覆盖延迟消息清理:

java
for (int i = 0; i < 100; i++) {
    handler.sendEmptyMessageDelayed(0, LONG_DELAY_MS);
}

resetQueue();

assertNull(queue.peekLastMessageForTest());

这里的输入是 100 条未来消息,动作是 resetForTest(),断言是队尾为空;它证明测试队列的 清理动作能移除待处理消息,不证明生产环境 quitSafely() 会以同样实现保留或丢弃所有未来消息。

测试文件的 syncWait() 还用 handler.post() 作为同步点,等待 Looper 处理一个普通消息后再 检查断言。这说明测试作者需要显式等待目标 Looper 前进,正好对应本文的核心边界:入队和实际 消费不是同一个时刻。

11. 复查路径 ​

源码文件:

  • frameworks/base/core/java/android/os/Handler.java
  • frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
  • frameworks/base/core/java/android/os/Looper.java
  • frameworks/base/core/java/android/os/SystemClock.java
  • frameworks/base/core/tests/coretests/src/android/os/MessageQueueTest.java

可以沿下面的搜索顺序复查一次延迟消息:

bash
rg -n "postDelayed|sendMessageDelayed|sendMessageAtTime|uptimeMillis" \
  frameworks/base/core/java/android/os/Handler.java

rg -n "enqueueMessage|nextPollTimeoutMillis|nativePollOnce|mQuitting" \
  frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java

rg -n "mQueue.next|dispatchMessage|dispatchStart|dispatchEnd" \
  frameworks/base/core/java/android/os/Looper.java

rg -n "uptimeMillis|elapsedRealtime|deep sleep" \
  frameworks/base/core/java/android/os/SystemClock.java

复述主线时,至少要回答四件事:相对延迟在哪里变成绝对 when,队列如何决定是否唤醒, next() 何时允许出队,以及当前 callback 为什么能把实际执行推迟到 when 之后。

12. 边界判断 ​

Handler.postDelayed 适合“在一个仍然运行的 Looper 线程上,按 uptime 时间安排一条消息”。 它提供了队列顺序和到期资格,不提供抢占当前 callback、跨深度睡眠的墙钟保证、进程死亡后的 持久化调度或取消后的 exactly-once 语义。

当你看到“延迟 100 ms 却 300 ms 才执行”的现象时,按源码顺序检查:投递时钟是否使用了正确的 时间基准,消息是否真的成功入队,队列头是否有屏障或更早消息,Looper 是否在 when 到达时 仍处理旧 callback,以及取消调用是否已经晚于 next() 摘除。这样得到的是可定位的时间分解, 而不是把所有晚点都归咎于“Handler 精度不够”。