IdleHandler 使用边界
MessageQueue.IdleHandler 经常被描述成“主线程空闲时执行一次的回调”,但这个说法少了几个 关键条件:队列可能仍然有未来消息;isIdle() 只查询原始队头;回调在队列锁外运行;返回 true 只保留注册,不提供固定周期;而且已经从队列摘下的消息不能再靠 IdleHandler 撤销。
本文面向已经读过 IdleHandler空闲回调、 MessageQueue取消息 和 消息延迟边界 的读者。HL023 负责解释 next() 的首轮快照、异常 和重查算法,本文把这些机制放到三个可操作问题中:如何判断“现在是否空闲”,谁拥有 IdleHandler 的注册状态,以及系统代码如何避免重复注册和长期持有不需要的回调。本文限定 Android 17 的 LegacyMessageQueue;CombinedMessageQueue 与 CombinedDeliMessageQueue 的 空闲实现另有分支,不从本文的字段和控制流外推结论。
读完后,读者应能区分点查询与事件回调,解释一个返回 true 的 handler 为什么可能很久不再 运行,定位 removeIdleHandler() 的所有者,并判断某个系统消费者的空闲回调是在做清理、状态 通知还是队列完成广播。
1. 语义分界
源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
接口注释把 IdleHandler 定义为:队列已经没有当前可处理的消息,即将等待更多消息时调用的 回调。队列里仍可存在未来消息,所以“空闲”不是“消息链表为空”,更不是“进程没有任何工作”。
public static interface IdleHandler {
/**
* Called when the message queue has run out of messages and will now wait
* for more. Return true to keep your idle handler active, false to have
* it removed.
*/
boolean queueIdle();
}同一个类还公开了 isIdle()。它是同步点查询:调用方立即得到一个布尔值,不会注册回调,也不 会等到 Looper 下一轮轮询。
public boolean isIdle() {
synchronized (this) {
final long now = SystemClock.uptimeMillis();
return mMessages == null || now < mMessages.when;
}
}两者关系可以写成:
| 能力 | 读取/执行者 | 触发方式 | 能否告诉你未来会何时再次触发 |
|---|---|---|---|
isIdle() | 任意查询线程 | 调用时读取 | 不能 |
queueIdle() | Looper 线程 | next() 首轮发现空闲 | 不能 |
postDelayed() | Looper 线程 | 到达 Message.when | 只能按消息时间排序 |
因此,isIdle() 适合诊断或条件判断;IdleHandler 适合把工作挂到队列下一次进入等待前, 但不能替代定时消息。
2. 队头判定
源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
isIdle() 读取的是 mMessages 原始头节点,不会像同步屏障路径那样跳过屏障去寻找异步消息。 判断只有两个分支:没有头节点,或头节点的 when 仍在未来。
final long now = SystemClock.uptimeMillis();
return mMessages == null || now < mMessages.when;| 原始队头 | isIdle() | 含义 |
|---|---|---|
null | true | 队列没有待处理消息 |
when > now | true | 队列有消息,但下一条尚未到期 |
when <= now | false | 队列头已有可处理消息 |
| 同步屏障 | 取决于屏障 when | 不等价于“后面没有可执行的异步消息” |
最后一行是实际使用中的陷阱:isIdle() 的 API 语义与 next() 的“寻找可分发异步消息”分支 不是同一套扫描。用 isIdle() 推断“下一次一定会执行某个异步消息”是不成立的。
3. 注册所有权
源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
队列把长期注册保存到 mIdleHandlers,把一次空闲批次的遍历对象保存到 mPendingIdleHandlers。注册和移除都在队列锁内完成;回调本身不会在这把锁里执行。
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);
}
}这里的所有权关系很具体:MessageQueue 持有 handler 引用,调用方持有能移除它的对象引用。 如果调用方只创建匿名对象而不保存引用,后续就只能等待它返回 false,不能主动取消。
ArrayList 不做去重。相同对象被添加两次,会在注册表中占据两个位置,并可能在一次快照中被 调用两次;使用者应把“是否已注册”作为自己的状态,而不是假设队列会替自己去重。
4. 首轮触发
源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
next() 用 pendingIdleHandlerCount = -1 标记一次调用的首轮。只有在没有到期消息且尚未 建立快照时,才读取 mIdleHandlers.size();如果队列随后再次循环,pendingIdleHandlerCount 会被置为 0,同一轮不会重复调用相同快照。
int pendingIdleHandlerCount = -1;
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 && now >= msg.when) {
// 摘除并返回一条到期消息
return msg;
}
if (pendingIdleHandlerCount < 0
&& (mMessages == null || now < mMessages.when)) {
pendingIdleHandlerCount = mIdleHandlers.size();
}
}
}首轮判定发生在“尝试取消息”之后。若原始队头已经到期,IdleHandler 不会抢在这条消息前面; 若只有未来消息,IdleHandler 可以在等待该消息期间先运行一次。
图中“记录注册数量”和“复制快照”是队列控制动作;queueIdle() 是用户代码,故意在锁外 执行。这个边界允许回调中调用 addIdleHandler() 或发送消息,而不会把队列锁带入业务代码。
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 不会插入正在遍历的数组,通常要等下一次空闲;
- 回调 A 在本批次移除的 handler 仍可能已经存在于数组中,因此不能用“移除”阻止本批次的 后续调用。
这不是并发 bug,而是“注册表状态”和“本批次遍历输入”被有意分离。若任务需要取消当前批次 的业务动作,应由任务自身设置取消状态,不能只依赖 removeIdleHandler()。
6. 返回状态
源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
queueIdle() 的返回值只决定当前对象是否留在 mIdleHandlers。它不改变 Looper 是否退出, 也不控制同一快照中的其他对象。
| 返回值 | 本次回调 | 以后空闲 |
|---|---|---|
false | 当前回调返回后结束 | 从注册表移除 |
true | 当前回调返回后结束 | 保留,等待下一次空闲批次 |
持续返回 true 也不等于固定周期。只要队列一直有到期消息,next() 就会优先交付这些消息; 只有再次进入空闲判定,handler 才有机会运行。
7. 异常处理
源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
IdleHandler 的异常被 next() 捕获并记录,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 消息:普通 callback 的未捕获异常由 Handler/Looper 的分发 路径处理;IdleHandler 则由 MessageQueue.next() 把异常转成日志并继续轮询。IdleHandler 内部 仍应自行保证资源清理,因为队列只负责移除注册对象,不知道业务资源处于什么状态。
8. 系统消费者
8.1 空闲清理
源码文件:frameworks/base/core/java/android/app/ActivityThread.java
ActivityThread 的 GcIdler 和 PurgeIdler 都返回 false。它们的调度状态由 mGcIdlerScheduled 与 mPurgeIdlerScheduled 管理,调度和取消路径显式成对出现。
final GcIdler mGcIdler = new GcIdler();
final PurgeIdler mPurgeIdler = new PurgeIdler();
boolean mGcIdlerScheduled = false;
boolean mPurgeIdlerScheduled = false;
final class GcIdler implements MessageQueue.IdleHandler {
public final boolean queueIdle() {
doGcIfNeeded();
purgePendingResources();
return false;
}
}
void scheduleGcIdler() {
if (!mGcIdlerScheduled) {
mGcIdlerScheduled = true;
Looper.myQueue().addIdleHandler(mGcIdler);
}
mH.removeMessages(H.GC_WHEN_IDLE);
}
void unscheduleGcIdler() {
if (mGcIdlerScheduled) {
mGcIdlerScheduled = false;
Looper.myQueue().removeIdleHandler(mGcIdler);
}
mH.removeMessages(H.GC_WHEN_IDLE);
}这里的“最佳实践”不是人为规定,而是源码中的所有权设计:ActivityThread 保存稳定的 idler 对象,用布尔字段防止重复加入,用显式移除取消尚未触发的清理。queueIdle() 返回 false 表示一次清理机会结束;doGcIfNeeded() 自己还会基于最近 GC 时间判断是否真的触发 GC, 所以 IdleHandler 触发不等于 GC 一定发生。
8.2 空闲检测
源码文件:frameworks/base/core/java/android/os/StrictMode.java
StrictMode 使用一个静态共享 handler 做实例计数检查,并在 queueIdle() 内再次按 uptimeMillis() 限制检查间隔。它返回 true,所以真正的生命周期由 VM policy 开关控制。
private static boolean sIsIdlerRegistered = false;
private static final MessageQueue.IdleHandler sProcessIdleHandler =
new MessageQueue.IdleHandler() {
public boolean queueIdle() {
long now = SystemClock.uptimeMillis();
if (now - sLastInstanceCountCheckMillis > 30 * 1000) {
sLastInstanceCountCheckMillis = now;
conditionallyCheckInstanceCounts();
}
return true;
}
};
if (!shouldCheckInstanceCountViolations(policy)) {
mq.removeIdleHandler(sProcessIdleHandler);
sIsIdlerRegistered = false;
} else if (!sIsIdlerRegistered) {
mq.addIdleHandler(sProcessIdleHandler);
sIsIdlerRegistered = true;
}这个消费者说明 return true 只能表达“保留注册”,不能表达“每次空闲都做昂贵工作”。 StrictMode 还在回调内部做 30 秒节流;关闭策略时则主动移除。若应用只写 return true 而没有 类似节流或退出条件,就会让对象长期挂在队列注册表上。
8.3 队列完成通知
源码文件:frameworks/base/core/java/android/speech/tts/TextToSpeechService.java
TTS 的 SynthThread 在自己的 HandlerThread.onLooperPrepared() 中注册 IdleHandler。第一次 空闲只翻转 mFirstIdle;后续空闲才广播队列处理完成,并且始终返回 true。
private class SynthThread extends HandlerThread implements MessageQueue.IdleHandler {
private boolean mFirstIdle = true;
@Override
protected void onLooperPrepared() {
getLooper().getQueue().addIdleHandler(this);
}
@Override
public boolean queueIdle() {
if (mFirstIdle) {
mFirstIdle = false;
} else {
broadcastTtsQueueProcessingCompleted();
}
return true;
}
}这个例子不能被简化成“空闲就代表所有业务完成”:它有意跳过首次空闲,并把后续空闲当作 合成队列处理完成的信号。它还在 onDestroy() 通过 getLooper().quit() 结束线程,所以注册 handler 的存活范围受 SynthThread 生命周期约束,而不是永久属于进程。
9. 测试输入
源码文件:frameworks/base/core/tests/coretests/src/android/os/IdleHandlerTest.java
测试用一个 HandlerThread 驱动输入,把 queueIdle() 次数写入计数器,再用延迟消息作为 “让队列再次忙起来”的边界。关键测试不是等待某个固定墙钟时刻,而是通过后续消息检查空闲 回调次数。
public void go() {
super.go();
mCount = 0;
mHandler.sendMessageDelayed(mHandler.obtainMessage(0), 100);
addIdleHandler();
}
public boolean queueIdle() {
mCount++;
return false;
}testOneShotFirst 的动作是先排入 100 ms 后的消息,再注册一次性 handler;收到第一条消息后 再排第二条消息,最终断言计数为 1。testRepeatedFirst 将返回值改为 true,两条延迟消息 之间最终断言计数为 2。它们证明的是“未来队头允许首次空闲”和 true/false 的注册差异, 不证明 IdleHandler 具有固定频率。
源码文件:frameworks/base/core/tests/coretests/src/android/os/MessageQueueTest.java
testResetClearsIdleHandlers 设计为:先让 handler 运行一次,调用 resetForTest(),再发送 普通消息;如果注册表已清空,第二次空闲不会再次减少 latch。但该测试当前被 @Ignore 标记, 因为使用的 TestLooperManager 不会可靠进入 idle。它可以说明 reset 的预期清理范围,不能作为 已执行通过的运行时断言。
10. 复查命令
源码文件:
frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.javaframeworks/base/core/java/android/app/ActivityThread.javaframeworks/base/core/java/android/os/StrictMode.javaframeworks/base/core/java/android/speech/tts/TextToSpeechService.javaframeworks/base/core/tests/coretests/src/android/os/IdleHandlerTest.javaframeworks/base/core/tests/coretests/src/android/os/MessageQueueTest.java
rg -n "isIdle|addIdleHandler|removeIdleHandler|pendingIdleHandlerCount|queueIdle" \
frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
rg -n "scheduleGcIdler|unscheduleGcIdler|GcIdler|PurgeIdler" \
frameworks/base/core/java/android/app/ActivityThread.java
rg -n "sProcessIdleHandler|sIsIdlerRegistered|queueIdle" \
frameworks/base/core/java/android/os/StrictMode.java
rg -n "SynthThread|onLooperPrepared|broadcastTtsQueueProcessingCompleted" \
frameworks/base/core/java/android/speech/tts/TextToSpeechService.java
rg -n "testOneShot|testRepeated|testResetClearsIdleHandlers" \
frameworks/base/core/tests/coretests/src/android/os/IdleHandlerTest.java \
frameworks/base/core/tests/coretests/src/android/os/MessageQueueTest.java阅读任一消费者时,先找三个符号:注册状态的 owner、queueIdle() 的返回值、取消或线程退出 路径。只看回调体而不看注册/移除,会把一次性清理、节流监控和持续完成通知误读成同一种用法。
11. 使用边界
IdleHandler 适合把短小、可被队列忙碌推迟的工作接到一个已有 Looper 的空闲窗口;它不适合 表达固定延迟、强实时截止时间、跨进程持久化或必须在当前批次立即取消的任务。回调运行在 Looper 线程,任何长时间 I/O 或复杂计算都会占用这条线程,并把下一条消息的处理继续向后推。
一个可审计的使用方式应满足:注册者保存 handler 引用;一次性任务返回 false;持续任务有 明确的节流和显式移除条件;回调内部的业务取消状态独立于队列移除;线程退出时不再把 idle 回调当作完成确认。这样使用时,IdleHandler 才是“空闲窗口通知”,而不是一个被误用的定时器 或后台执行器。
