Skip to content

IdleHandler 空闲回调

基于 LegacyMessageQueue 源码追踪空闲判定、回调快照、keep 返回值、异常和队列重查。

基于android-17.0.0_r1
AndroidIdleHandlerMessageQueueLooper源码阅读

IdleHandler 空闲回调 ​

本文只分析 Android 17 LegacyMessageQueue/MessageQueue.java 中的 IdleHandler。源码树还包含 CombinedMessageQueue 和 CombinedDeliMessageQueue,它们有各自的 next() 实现,不能把本文的行号和控制流直接外推到那些实现。

读者可先阅读 MessageQueue取消息 了解 next() 的消息摘除、超时和同步屏障背景。本文进一步回答:队列何时被称为空闲、回调为什么只在一次 next() 的首轮运行、queueIdle() 的返回值如何改变注册表,以及回调期间新消息为什么会被立即重新检查。

1. 接口契约 ​

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

java
public static interface IdleHandler {
    boolean queueIdle();
}

接口注释把语义限定为“队列已经没有当前可处理的消息,即将等待更多消息”。它还明确允许一种反直觉情况:队列中仍有消息,但队头消息安排在未来,此时也可以调用 queueIdle()。因此 IdleHandler 不是定时器,也不是“Looper 线程永远没有任何任务”的通知。

2. 注册状态 ​

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

java
private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();
private IdleHandler[] mPendingIdleHandlers;

public void addIdleHandler(@NonNull IdleHandler handler) {
    if (handler == null) {
        throw new NullPointerException("Can't add a null IdleHandler");
    }
    synchronized (this) {
        mIdleHandlers.add(handler);
    }
}

public void removeIdleHandler(@NonNull IdleHandler handler) {
    synchronized (this) {
        mIdleHandlers.remove(handler);
    }
}

mIdleHandlers 是长期注册表,mPendingIdleHandlers 是一次空闲批次的数组快照。注册和移除都可从任意线程调用,因为它们使用与消息链表相同的锁。空 handler 在进入锁前就抛出 NullPointerException;移除不存在的对象则没有额外结果。

3. 空闲判定 ​

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

isIdle() 的公开实现直接检查原始队头:

java
public boolean isIdle() {
    synchronized (this) {
        final long now = SystemClock.uptimeMillis();
        return mMessages == null || now < mMessages.when;
    }
}

next() 的 IdleHandler 判定使用同一条件:mMessages == null,或 now < mMessages.when。这里看的是链表原始头节点,不是跳过同步屏障后找到的异步消息。因此一个已经到期的同步屏障会使队列不满足 idle 条件,即使后面存在尚未到期的异步消息。

“队列空闲”具体有两种状态:

状态原始队头下一步
真空mMessages == nullnative 无限等待,或先运行 IdleHandler
未来队头now < mMessages.when以队头时间计算 timeout,或先运行 IdleHandler
到期队头now >= mMessages.when摘除并交付消息,不运行本轮 IdleHandler

4. 首轮入口 ​

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

java
int pendingIdleHandlerCount = -1;
int nextPollTimeoutMillis = 0;
for (;;) {
    if (nextPollTimeoutMillis != 0) {
        Binder.flushPendingCommands();
    }
    nativePollOnce(ptr, nextPollTimeoutMillis);

    synchronized (this) {
        // 先尝试取一条到期消息;没有消息时才进入 idle 判定。
        if (pendingIdleHandlerCount < 0
                && (mMessages == null || now < mMessages.when)) {
            pendingIdleHandlerCount = mIdleHandlers.size();
        }
    }
}

pendingIdleHandlerCount 的初值 -1 是一次 next() 调用的首轮标记。第一次发现空闲时,它读取注册表大小;回调执行完会将该变量设为 0,所以同一次 next() 即使再次循环,也不会立刻重复执行同一批 IdleHandler。

5. 快照执行 ​

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

锁内只扩容并填充数组,用户代码在锁外执行:

java
if (mPendingIdleHandlers == null) {
    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
}

for (int i = 0; i < pendingIdleHandlerCount; i++) {
    final IdleHandler idler = mPendingIdleHandlers[i];
    mPendingIdleHandlers[i] = null;

    boolean keep = false;
    try {
        keep = idler.queueIdle();
    } catch (Throwable t) {
        Log.wtf(TAG, "IdleHandler threw exception", t);
    }
    if (!keep) {
        synchronized (this) {
            mIdleHandlers.remove(idler);
        }
    }
}

快照意味着本批次的遍历对象已经确定。回调 A 新增的 handler 不会插入当前数组,但可以参加下一次 idle;回调 A 移除回调 B,也不会从当前数组删除 B,B 仍可能在本批次执行。mPendingIdleHandlers[i] = null 则在单个回调取出后立即释放数组槽中的引用。

6. 返回语义 ​

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

queueIdle() 返回值只决定长期注册表是否保留当前对象:

返回值当前批次后续空闲
true本次执行完成mIdleHandlers 保留对象
false本次执行完成从 mIdleHandlers 移除

返回 false 不是“停止 Looper”,也不是“跳过当前批次后面的 handler”;它只影响当前 idler 的未来注册状态。一个一次性初始化任务应返回 false;持续监听队列空闲则返回 true,但它仍受消息密度影响,不保证按固定周期运行。

7. 异常路径 ​

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

实现捕获 Throwable,并将 keep 预置为 false:

java
boolean keep = false;
try {
    keep = idler.queueIdle();
} catch (Throwable t) {
    Log.wtf(TAG, "IdleHandler threw exception", t);
}

if (!keep) {
    synchronized (this) {
        mIdleHandlers.remove(idler);
    }
}

因此抛出异常的 handler 在本次执行后按 false 处理并被移除,异常不会沿 next() 继续抛给 Looper.loopOnce()。这条异常策略只适用于 IdleHandler;普通 Handler 消息的异常路径由 Handler.dispatchMessage() 和 Looper 的外层控制流决定,不能混为一谈。

8. 回调后重查 ​

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

java
pendingIdleHandlerCount = 0;
nextPollTimeoutMillis = 0;

回调可能调用 Handler.sendMessage(),使队列出现一条新消息;也可能移除原来的队头。将 timeout 重置为 0 后,外层循环会立刻回到 nativePollOnce(ptr, 0),重新读取 Java 链表,而不是继续等待旧的未来时间。

9. 退出清理 ​

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

退出判断在 IdleHandler 判定之前:

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

如果队列正在退出,next() 不会先运行一批 IdleHandler 再退出,而是释放 native Looper 指针并返回 null。resetForTest() 是不同路径:它在未退出时清空 mIdleHandlers、消息和 FD 记录,然后 nativeWake(mPtr);如果 mQuitting 已经为真,则直接返回,不能把已退出队列复活。

java
public void resetForTest() {
    synchronized (this) {
        if (mQuitting) {
            return;
        }
        mIdleHandlers.clear();
        removeAllFdRecords();
        removeAllMessagesLocked();
        resetSyncBarrierTokens();
        nativeWake(mPtr);
    }
}

10. 系统消费者 ​

源码文件:frameworks/base/core/java/android/app/ActivityThread.java

ActivityThread 使用返回 false 的 idler 延后 GC 和 purge 工作:

java
Looper.myQueue().addIdleHandler(mGcIdler);
Looper.myQueue().removeIdleHandler(mGcIdler);

这里的调用方式体现了两个事实:注册对象需要由拥有者保存,才能在状态变化时显式移除;GcIdler 和 PurgeIdler 返回 false,并在实际工作路径中重置对应 scheduled 标志,让后续请求可以再次注册。另一个真实消费者是 StrictMode,它用静态共享 idler 和 sIsIdlerRegistered 记录注册状态,根据 VM policy 选择添加或移除。

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

java
if (!shouldCheckInstanceCountViolations(policy)) {
    mq.removeIdleHandler(sProcessIdleHandler);
    sIsIdlerRegistered = false;
} else if (!sIsIdlerRegistered) {
    mq.addIdleHandler(sProcessIdleHandler);
    sIsIdlerRegistered = true;
}

StrictMode 的 queueIdle() 返回 true,因此只需在策略关闭时显式移除;布尔标志避免策略重复设置时把同一个对象多次加入 ArrayList。

11. 测试输入 ​

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

测试使用 TestHandlerThread,把 handler 的 what 消息与 queueIdle() 次数绑定:

测试输入序列关键断言
testOneShotFirst先排入 100ms 后消息,再注册 idler;收到第一条消息后再排第二条return false 的 idler 计数为 1
testOneShotLater先处理普通消息,再在其回调中注册 idler,并排入未来消息仍只执行 1 次
testRepeatedFirst初始就有未来消息,idler 返回 true两次消息之间出现两次 idle
testRepeatedLater普通消息回调中注册 idler,随后安排两条未来消息返回 true 的 idler 执行 2 次

这些测试证明“未来队头也能触发 idle”和 true/false 的保留差异;它们不证明 IdleHandler 有实时调度保证,也不覆盖抛出 Throwable 的日志输出。

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

testResetClearsIdleHandlers 先让 idler 完成一次,再调用 resetForTest(),随后发送普通消息并等待;断言计数不再增加。该测试当前标记为 ignored,因为测试使用的 TestLooperManager 本身不会可靠进入 idle;它仍准确记录了 reset 应清空注册表的目标,以及该场景的测试限制。

12. 阅读验证 ​

bash
rg -n "pendingIdleHandlerCount|mPendingIdleHandlers|queueIdle|nextPollTimeoutMillis" \
  frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java

rg -n "testOneShot|testRepeated|IdleHandler" \
  frameworks/base/core/tests/coretests/src/android/os/IdleHandlerTest.java \
  frameworks/base/core/tests/coretests/src/android/os/MessageQueueTest.java

rg -n "addIdleHandler|removeIdleHandler|queueIdle" \
  frameworks/base/core/java/android/app/ActivityThread.java \
  frameworks/base/core/java/android/os/StrictMode.java

阅读 next() 时可以手动模拟三种输入:到期普通消息、未来队头、到期同步屏障。分别判断 IdleHandler 是否运行、pendingIdleHandlerCount 何时从 -1 变成 0、回调后为什么必须使用 timeout 0 重查。

13. 适用范围 ​

本文结论只适用于 Android 17 的 LegacyMessageQueue 实现,覆盖 IdleHandler 的注册、首轮空闲判定、快照、锁外执行、返回值、异常、重查和测试清理。它不替代同步屏障专题,不把 IdleHandler 当作定时器,也不声称 CombinedMessageQueue 与 CombinedDeliMessageQueue 使用完全相同的内部算法。