1. 泄漏现象实录:崩溃堆栈、ANR日志与内存抖动曲线

Android 系统中,由 Handler 引发的内存泄漏极具迷惑性——它在崩溃前往往有一段相当长的潜伏期。本章的任务是从现场证据入手,掌握识别此类泄漏的“第一眼判断法”。

1.1 典型崩溃堆栈分析

以下是一条来自线上用户反馈的崩溃日志核心片段。注意其中的关键行:

java.lang.RuntimeException: Unable to start activity ComponentInfo{.../MainActivity}
    at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2955)
    ...
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method
    'void android.widget.TextView.setText(java.lang.CharSequence)'
    on a null object reference
    at com.example.app.MainActivity$MyHandler.handleMessage(MainActivity.java:89)
    at android.os.Handler.dispatchMessage(Handler.java:106)
    at android.os.Looper.loop(Looper.java:214)
    at android.app.ActivityThread.main(ActivityThread.java:7342)
挖掘点:崩溃发生在 handleMessage() 中,试图操作一个已经被销毁的 Activity 的视图。
LeakCanary / MAT 快照在此场景下往往显示:持有 MainActivity 实例的 Handler 虽已被 GC 回收,但 MessageQueue 中仍存在一个未处理的 Message,该 Message 的 target 指向已经被回收的 Handler 的弱引用所在的匿名内部类,导致实际 Activity 无法释放。

1.2 ANR 日志中的隐藏信号

当泄漏导致消息队列持续堆积,主线程会被长时间阻塞。ANR 日志中你会看到这样的模式:

ANR in com.example.app (com.example.app/.MainActivity)
  Reason: Input dispatching timed out (Waiting to send non-key event
      because the touched window has not finished processing certain
      input events that were delivered to it over 500.0ms ago)
  Load: 17.72 / 13.23 / 8.78
  CPU usage from 13223ms to 12322ms ago (2025-02-14 14:23:11.122 to 2025-02-14 14:23:22.143):
    59% 12345/app: 42% user + 17% kernel / faults: 12345 minor 12 major
  ...
  ----- pid 12345 at 2025-02-14 14:23:22.143 -----
  DALVIK THREADS (45):
  "main" prio=5 tid=1 Runnable
    | group="main" sCount=0 dsCount=0 flags=0 obj=0x... self=0x...
    | sysTid=12345 nice=-2 cgrp=default sched=0/0 handle=0xf...
    | state=R schedstat=( 123456789 9876543 1234 ) utm=123 stm=45 core=0
    at com.example.app.MainActivity$MyHandler.handleMessage(MainActivity.java:89)
    at android.os.Handler.dispatchMessage(Handler.java:106)
    at android.os.Looper.loop(Looper.java:214)
    at android.app.ActivityThread.main(ActivityThread.java:7342)

  "Thread-12" prio=5 tid=24 TimedWaiting
    at java.lang.Thread.sleep(Native Method)
    at com.example.app.SomeBackgroundWorker.run(SomeBackgroundWorker.java:45)
ANR 日志的 main 线程堆栈如果反复出现 Handler.dispatchMessage → handleMessage,就说明消息队列正在被缓慢泄漏的消息撑爆。配合 Load 值持续超过 10,CPU 使用率长时间 >50%,基本可以判定存在 Handler 相关的主线程阻塞。

1.3 内存抖动曲线与 GC 频繁触发

使用 Profiler 抓取一段 60 秒的内存使用曲线,你会看到典型的“锯齿状”抖动模式:

时间窗口内存占用 (MB)GC 触发次数GC 类型抖动幅度
0-15s120 → 1453Concurrent~25MB
15-30s140 → 1757Concurrent + Alloc~35MB
30-45s170 → 23012Alloc (频繁)
→ 转为 Full
~60MB
45-60s225 → 180 (GC after)15Full GC恢复后仍有残留 15-20MB

这条曲线的关键特征:

  • 持续上升:每次消息发送都分配新对象(例如 Bitmap、大字符串、View 引用),但无法被及时回收。
  • 全量 GC 后残留:Handler 回调中持有对 Activity 的强引用,导致 Activity 的视图层级无法被回收,即使 GC 后内存基线也逐步抬高。
  • GC 频率指数级上升:从最初的 3 次/15s 飙升至 15 次/15s,最终触发 ANR 或 OOM。

1.4 排查表:从现场证据到初步判断

面对上述现象,你可以用以下排查表快速锁定可疑方向:

关键判断规则:如果崩溃发生在 handleMessage() 中且消息来自 postDelayed,或者 ANR 主线程堆栈驻留在 Handler.dispatchMessage 中,那么 Handler + Lifecycle 泄漏的可能性超过 80%。此时应优先检查消息发送时刻与 Activity/Fragment 生命周期的交叉点。

1.5 故障树:Handler 耦合泄漏的现象链路

下面的故障树展示了从现象到根因的完整路径。本节课后续所有章节都将围绕这棵树的某一分支展开深入排查。

Handler 耦合泄漏故障树 – 现象链路 Handler 耦合泄漏 崩溃堆栈 ANR 日志 内存抖动曲线 GC 频繁触发 Handler 持有
已销毁 Activity 消息队列堆积
主线程卡顿
频繁创建对象
残留引用
生命周期错配
GC 无法回收
生命周期不匹配判定 移除回调 / 弱引用 / Lifecycle 绑定 profiler 复测 + 静态检查 + lint 规则
本章核心收获:当你看到上述四种现象中的两种或以上并存时,就可以启动“Handler 生命周期耦合泄漏”的专项排查流程。从第 2 章开始,我们将依次展开数据采集(dump 堆栈与 Profiler 录制)、原因定位(检查消息队列与生命周期钩子)、修复动作(移除、解绑与弱引用改造)、复测验证(临界场景对比)以及静态预防(Lint 规则与 Code Review 清单)。