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)
挖掘点:崩溃发生在
LeakCanary / MAT 快照在此场景下往往显示:持有 MainActivity 实例的 Handler 虽已被 GC 回收,但 MessageQueue 中仍存在一个未处理的 Message,该 Message 的 target 指向已经被回收的 Handler 的弱引用所在的匿名内部类,导致实际 Activity 无法释放。
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-15s | 120 → 145 | 3 | Concurrent | ~25MB |
| 15-30s | 140 → 175 | 7 | Concurrent + Alloc | ~35MB |
| 30-45s | 170 → 230 | 12 | Alloc (频繁) → 转为 Full | ~60MB |
| 45-60s | 225 → 180 (GC after) | 15 | Full 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 生命周期耦合泄漏”的专项排查流程。从第 2 章开始,我们将依次展开数据采集(dump 堆栈与 Profiler 录制)、原因定位(检查消息队列与生命周期钩子)、修复动作(移除、解绑与弱引用改造)、复测验证(临界场景对比)以及静态预防(Lint 规则与 Code Review 清单)。