IdleHandler 空闲回调
本文只分析 Android 17 LegacyMessageQueue/MessageQueue.java 中的 IdleHandler。源码树还包含 CombinedMessageQueue 和 CombinedDeliMessageQueue,它们有各自的 next() 实现,不能把本文的行号和控制流直接外推到那些实现。
读者可先阅读 MessageQueue取消息 了解 next() 的消息摘除、超时和同步屏障背景。本文进一步回答:队列何时被称为空闲、回调为什么只在一次 next() 的首轮运行、queueIdle() 的返回值如何改变注册表,以及回调期间新消息为什么会被立即重新检查。
1. 接口契约
源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
public static interface IdleHandler {
boolean queueIdle();
}接口注释把语义限定为“队列已经没有当前可处理的消息,即将等待更多消息”。它还明确允许一种反直觉情况:队列中仍有消息,但队头消息安排在未来,此时也可以调用 queueIdle()。因此 IdleHandler 不是定时器,也不是“Looper 线程永远没有任何任务”的通知。
2. 注册状态
源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.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() 的公开实现直接检查原始队头:
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 == null | native 无限等待,或先运行 IdleHandler |
| 未来队头 | now < mMessages.when | 以队头时间计算 timeout,或先运行 IdleHandler |
| 到期队头 | now >= mMessages.when | 摘除并交付消息,不运行本轮 IdleHandler |
4. 首轮入口
源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.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
锁内只扩容并填充数组,用户代码在锁外执行:
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:
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
pendingIdleHandlerCount = 0;
nextPollTimeoutMillis = 0;回调可能调用 Handler.sendMessage(),使队列出现一条新消息;也可能移除原来的队头。将 timeout 重置为 0 后,外层循环会立刻回到 nativePollOnce(ptr, 0),重新读取 Java 链表,而不是继续等待旧的未来时间。
9. 退出清理
源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
退出判断在 IdleHandler 判定之前:
if (mQuitting) {
dispose();
return null;
}如果队列正在退出,next() 不会先运行一批 IdleHandler 再退出,而是释放 native Looper 指针并返回 null。resetForTest() 是不同路径:它在未退出时清空 mIdleHandlers、消息和 FD 记录,然后 nativeWake(mPtr);如果 mQuitting 已经为真,则直接返回,不能把已退出队列复活。
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 工作:
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
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. 阅读验证
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 使用完全相同的内部算法。
