Handler 引用链排查
“Handler 导致 Activity 泄漏”不是一个由类名直接决定的结论。Android 17 的真实引用链是: MessageQueue 持有待处理 Message,Message 的 target 持有 Handler,callback 或 obj 可能 继续持有业务对象;只有当这些引用从 GC root 可达,且消息在队列中停留足够久,才形成可观察 的保留。Message 被处理、移除、队列退出或业务自身不再持有对象,都会改变结果。
本文面向已经读过 Handler消息发送、 Message对象池、消息延迟边界 和 IdleHandler 使用边界 的读者。本文只 依据 Handler、Message、Legacy MessageQueue 和固定 framework 用法讲可达性与清理,不引入 LeakCanary 的实现或未经源码支持的 Activity 示例作为事实。
读完后,读者应能从 heap 引用链区分 target、callback、obj 三种持有来源,判断 removeCallbacksAndMessages() 能否覆盖某条消息,解释消息已经出队后为何无法撤回,并把 Handler 泄漏、Runnable 闭包保留和 HandlerThread 生命周期分开排查。
1. 引用字段
源码文件:frameworks/base/core/java/android/os/Message.java
Message 明确保存 Handler target、Runnable callback、任意对象 obj 和 next 链接。MessageQueue 的待处理链表通过这些字段把一次投递和消费者、载荷连接起来。
/*package*/ Handler target;
/*package*/ Runnable callback;
/*package*/ Object obj;
/*package*/ Message next;一条典型的可达链可以写成:
Looper/MessageQueue
-> mMessages
-> Message.target -> Handler
-> Message.callback -> Runnable/闭包
-> Runnable 捕获的外部对象另一条链从 Message.obj 出发:
MessageQueue -> Message.obj -> Activity/Context/业务状态obj 并不只保存简单值;ActivityThread、ViewRootImpl、系统服务都用它承载状态对象。因此 排查不能只搜索匿名 Handler,还要检查每个延迟 Message 的 payload。
2. 入队持有
源码文件:frameworks/base/core/java/android/os/Handler.java
postDelayed() 把 Runnable 放进 Message.callback;sendMessage() 则把调用方提供的 Message 交给 Handler,统一入队点写入 target。
public final boolean postDelayed(@NonNull Runnable r, long delayMillis) {
return sendMessageDelayed(getPostMessage(r), delayMillis);
}
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
return queue.enqueueMessage(msg, uptimeMillis);
}这说明 Handler 本身不是唯一保留源:
| 投递形式 | 直接引用 | 需要继续检查 |
|---|---|---|
post(r) | Message.callback = r | Runnable 是否捕获外部对象 |
sendMessage(msg) | Message.target + msg.obj | obj 是否持有 Activity/Context |
| Handler 非静态内部类 | Message.target → Handler | Handler 是否隐式持有外部实例 |
3. 非静态警告
源码文件:frameworks/base/core/java/android/os/Handler.java
Handler 构造器提供 FIND_POTENTIAL_LEAKS 调试开关。开启后,如果 Handler 是匿名、成员或 局部非静态类,源码只打印“可能泄漏”的警告。
if (FIND_POTENTIAL_LEAKS) {
final Class<? extends Handler> klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass())
&& (klass.getModifiers() & Modifier.STATIC) == 0) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: "
+ klass.getCanonicalName());
}
}这里的关键词是 might occur:警告不是泄漏证明。若消息立即处理、Handler 生命周期短、 外部对象没有被 Handler 捕获,实际 heap 可能没有长时间保留;反过来,静态 Handler 的 Runnable/obj 仍可能持有 Activity。
4. Runnable 捕获
源码文件:frameworks/base/core/java/android/os/Handler.java
匿名 Runnable 是 callback 引用的实际对象。即使 Handler 是静态或来自主 Looper,Runnable 仍可能通过编译器生成的外部引用持有 Activity、View 或 Fragment。
final Runnable task = () -> updateView();
handler.postDelayed(task, 60_000);排查这类链路时,heap 中应继续展开 Message.callback,查看 Runnable 实现类的字段;不要只 看到 Message.target 已经是一个“静态 Handler”就结束分析。若 Runnable 改为只持有 WeakReference,则回调执行时必须处理 referent 已为空的分支。
5. 载荷持有
源码文件:frameworks/base/core/java/android/os/Message.java
Message 的 obj 是普通 Object 引用,Message.toString() 也会把它输出到调试文本。因此延迟 消息可能通过 obj 保留比 Handler 更大的对象图。
if (obj != null) {
b.append(" obj=");
b.append(obj);
}
b.append(" target=");
b.append(target.getClass().getName());例如 ViewRootImpl 的 resize Message 把 SomeArgs 作为 obj,系统服务把 ProcessRecord、 WindowContainer 或 Provider 状态作为 obj;这些对象是否应存活取决于对应的取消和消费路径。 不能把所有 obj 都当成泄漏,但必须确认其 owner 和清理时机。
6. 队列清理
源码文件:frameworks/base/core/java/android/os/Handler.java
removeCallbacksAndMessages(token) 把清理请求交给 MessageQueue;token 为 null 时匹配该 Handler 的所有 callback 和 message,非 null 时按 Message.obj == token 匹配。
public final void removeCallbacksAndMessages(@Nullable Object token) {
mQueue.removeCallbacksAndMessages(this, token);
}它覆盖的是“仍在队列链表中、target 是这个 Handler”的消息。已由 next() 摘下的消息不再 属于链表,不能被这个 API 撤回。
7. 精确移除
源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
按 Runnable 移除时,队列同时匹配 Handler、callback 和可选 obj;按 what/obj 移除时则匹配 Handler、what 和可选 obj。移除时调用 recycleUnchecked(),释放 Message 的引用字段。
void removeMessages(Handler h, Runnable r, Object object) {
synchronized (this) {
Message p = mMessages;
while (p != null) {
Message n = p.next;
if (n != null && n.target == h && n.callback == r
&& (object == null || n.obj == object)) {
p.next = n.next;
n.recycleUnchecked();
continue;
}
p = n;
}
}
}引用匹配使用 == 的 API 不能用一个内容相等但不是同一实例的 token 替代;平台另有隐藏的 removeEqualMessages(),但它明确警告错误的 equals 实现可能删除无关事件。
8. Message 回收
源码文件:frameworks/base/core/java/android/os/Message.java
Legacy 回收路径的 clear() 把 obj、target、callback、next 等字段清零,然后把 Message 放回对象池。对象池本身可能长期存活,但清零后的 Message 不再通过这些字段保持业务对象。
void clear() {
flags = FLAG_IN_USE;
what = 0;
obj = null;
target = null;
callback = null;
next = null;
data = null;
}Concurrent MessageQueue 实现使用 clearReferenceFields() 保留移除标记和链结构,但把 obj、target、callback 替换为 sentinel,目的是避免移除线程继续通过引用字段抓住业务对象。
void clearReferenceFields() {
obj = NULL_OBJECT;
target = (target != null) ? NULL_HANDLER : null;
callback = NULL_RUNNABLE;
}9. 出队窗口
源码文件:frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
next() 找到到期消息后,会把它从 mMessages 链表摘下并设置 msg.next = null,再返回给 Looper。此时 Message 仍持有 target/callback,直到 Looper 分发完并 recycle。
msg.next = null;
msg.markInUse();
return msg;这形成取消的临界窗口:
| 时机 | Message 是否还在队列 | removeCallbacks 是否有效 |
|---|---|---|
| 延迟未到期 | 是 | 通常有效 |
next() 已摘下 | 否 | 无法撤回 |
| Runnable 已运行 | 否 | 只能由业务状态自行取消 |
因此生命周期清理应同时使用队列移除和 Runnable/Handler 内部的取消标志;单独调用移除 API 不能覆盖所有执行窗口。
10. 生命周期清理
源码文件:frameworks/base/core/java/android/view/ViewRootImpl.java
当前 framework 的真实消费者会在状态结束时移除对应消息。例如 ViewRootImpl 取消延迟失效消息 时,会按 View 对象清理 MSG_INVALIDATE 和 MSG_INVALIDATE_RECT,同时清理 animation invalidation 记录。
public void cancelInvalidate(View view) {
mHandler.removeMessages(MSG_INVALIDATE, view);
mHandler.removeMessages(MSG_INVALIDATE_RECT, view);
mInvalidateOnAnimationRunnable.removeView(view);
}这里的清理 owner 是 ViewRootImpl,不是 Looper;Looper 只执行队列操作。任何把 Activity/View 放进延迟消息的组件,都应由拥有该对象生命周期的类保存 token/Runnable,并在 detach/destroy 路径中调用精确移除。
11. 线程生命周期
源码文件:frameworks/base/core/java/android/os/HandlerThread.java
HandlerThread 自己是 Thread,run() 创建 Looper 后进入循环;只要线程仍运行,Looper 和 MessageQueue 就可能继续持有 Handler/Message 链。没有 quit()/quitSafely() 的 owner 清理, 线程本身就可能成为长期 GC root 路径。
@Override
public void run() {
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
onLooperPrepared();
Looper.loop();
}退出 HandlerThread 只能终止 Looper 消费;它不会替业务关闭外部 Cursor、socket、callback 或 当前正在执行的 Runnable。线程停止和对象释放是两个需要分别确认的结果。
12. 弱引用边界
源码文件:frameworks/base/core/java/android/view/InputEventReceiver.java
framework 的 InputEventReceiver native peer 使用 Java receiver 的 WeakReference,防止 native callback 反向强持有 Java receiver;但 Java receiver 自己仍保存 native pointer、Looper 和 sequence map,必须显式 dispose()。
mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),
inputChannel, mLooper.getQueue());弱引用只打断一条特定的 native→Java 强引用链,不能自动清理 MessageQueue 中由 Handler target 或 callback 形成的业务引用。排查时要确认是哪一条边需要弱引用,哪一条边需要显式注销。
13. 调试输入
源码文件:frameworks/base/core/java/android/os/Message.java
调试引用链时可用 Message 的 toString(long now) 查看 when、callback、what、obj 和 target 类型;但它只描述 Message 当前字段,不等于 heap dump 的完整可达性证明。
String toString(long now) {
StringBuilder b = new StringBuilder();
b.append("{ when=");
TimeUtils.formatDuration(when - now, b);
if (callback != null) {
b.append(" callback=");
b.append(callback.getClass().getName());
} else {
b.append(" what=");
b.append(what);
}
b.append(" target=");
b.append(target.getClass().getName());
return b.toString();
}实际排查应把 Message dump、Handler 对象字段、Runnable 捕获字段和 Activity 生命周期时间线 结合起来;不能只根据一个 target 类名下结论。
14. 验证输入
源码文件:frameworks/base/core/java/android/os/Handler.java
第一组验证构造 pending 链:投递带长延迟的 Runnable 和带 token 的 Message,检查队列中 callback/target/obj;调用 removeCallbacksAndMessages(token) 后确认匹配消息被回收,非匹配 消息仍在队列中。
第二组验证出队竞态:让目标 Looper 在 next() 摘下消息后暂停,另一线程调用 removeCallbacks();断言 Runnable 仍可能执行,说明移除 API 只覆盖队列内消息。
第三组验证 HandlerThread:启动 worker,投递持有外部对象的延迟 Runnable,执行 quitSafely(),分别观察未来消息被清理、线程结束和外部对象是否仍被其它引用持有。不能把 线程结束直接当成 Activity 已可回收。
15. 复查命令
源码文件:
frameworks/base/core/java/android/os/Handler.javaframeworks/base/core/java/android/os/Message.javaframeworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.javaframeworks/base/core/java/android/os/HandlerThread.javaframeworks/base/core/java/android/view/ViewRootImpl.java
rg -n "target|callback|obj|clear\(|clearReferenceFields|recycleUnchecked" \
frameworks/base/core/java/android/os/Message.java
rg -n "removeCallbacks|removeMessages|removeCallbacksAndMessages|FIND_POTENTIAL_LEAKS" \
frameworks/base/core/java/android/os/Handler.java
rg -n "removeMessages\(|removeCallbacksAndMessages|msg\.next = null|mQuitting" \
frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
rg -n "quit\(|quitSafely|Looper\.loop|cancelInvalidate" \
frameworks/base/core/java/android/os/HandlerThread.java \
frameworks/base/core/java/android/view/ViewRootImpl.java排查时按“GC root → MessageQueue → Message → target/callback/obj → 外部对象”走引用链,再确认 消息是否仍在队列、是否已出队、谁拥有取消 token,以及 HandlerThread/IdleHandler/观察者是否 仍然注册。这样才能区分真实可达链和仅存在于代码结构中的潜在风险。
16. 适用边界
Handler 泄漏排查不是“所有匿名 Handler 都改成静态类”的机械重构。真实结论取决于 Message 字段引用、队列停留时间、对象生命周期、取消路径和其它 GC root。静态 Handler、WeakReference、 精确移除和 HandlerThread quit 都只是打断特定链路的工具,不能互相替代。
当 heap 中发现 Activity 仍存活时,先确认是 Message.target、Message.callback、 Message.obj、IdleHandler、native peer 还是外部观察者持有;再回到对应 owner 的销毁/注销 路径。只有把引用来源和生效时机都对上源码,才能判断修复是否真的切断了链。
