Skip to content

Handler 引用链排查

从 Message 的 target/callback/obj 引用和 MessageQueue 清理路径,分析 Handler 延迟任务的可达性、取消窗口与线程生命周期。

基于android-17.0.0_r1
AndroidHandlerMessageQueue内存分析源码阅读

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 的待处理链表通过这些字段把一次投递和消费者、载荷连接起来。

java
/*package*/ Handler target;
/*package*/ Runnable callback;
/*package*/ Object obj;
/*package*/ Message next;

一条典型的可达链可以写成:

text
Looper/MessageQueue
  -> mMessages
  -> Message.target -> Handler
  -> Message.callback -> Runnable/闭包
  -> Runnable 捕获的外部对象

另一条链从 Message.obj 出发:

text
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。

java
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 = rRunnable 是否捕获外部对象
sendMessage(msg)Message.target + msg.objobj 是否持有 Activity/Context
Handler 非静态内部类Message.target → HandlerHandler 是否隐式持有外部实例

3. 非静态警告 ​

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

Handler 构造器提供 FIND_POTENTIAL_LEAKS 调试开关。开启后,如果 Handler 是匿名、成员或 局部非静态类,源码只打印“可能泄漏”的警告。

java
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。

java
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 更大的对象图。

java
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 匹配。

java
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 的引用字段。

java
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 不再通过这些字段保持业务对象。

java
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,目的是避免移除线程继续通过引用字段抓住业务对象。

java
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。

java
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 记录。

java
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 路径。

java
@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()。

java
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 的完整可达性证明。

java
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.java
  • frameworks/base/core/java/android/os/Message.java
  • frameworks/base/core/java/android/os/LegacyMessageQueue/MessageQueue.java
  • frameworks/base/core/java/android/os/HandlerThread.java
  • frameworks/base/core/java/android/view/ViewRootImpl.java
bash
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 的销毁/注销 路径。只有把引用来源和生效时机都对上源码,才能判断修复是否真的切断了链。