第4章 第一个泄漏检测实战:创建Handler匿名内部类、触发泄漏、查看Dump结果
4.1 本章目标与原理
Handler 匿名内部类是 Android 中最常见的内存泄漏来源之一。当在 Activity 内部通过匿名内部类创建 Handler 时,该内部类会隐式持有外部 Activity 的引用。如果该 Handler 向主线程 MessageQueue 发送了一个延迟消息(例如 sendMessageDelayed 或 postDelayed),且 Activity 在延迟到达前被销毁,则整个 Activity 实例因被该 Handler 对象引用链可达,而无法被 GC 回收,造成泄漏。
关键点:非静态内部类(包括匿名内部类)会持有外部类的强引用,这是 Handler 泄漏的根因。解决方案是使用静态内部类 + 弱引用,或在 Activity onDestroy 中移除所有回调和消息。
4.2 创建实验工程
在 Android Studio 中新建一个空 Activity 项目,包名为 com.example.leakdemo。在 MainActivity 中编写以下代码创建泄漏场景:
public class MainActivity extends AppCompatActivity {
// 匿名内部类 Handler(泄漏根源)
private final Handler leakyHandler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
// 模拟耗时操作
super.handleMessage(msg);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 发送一个延迟 10 分钟的消息,确保 Activity 销毁时消息未处理
leakyHandler.sendEmptyMessageDelayed(0, 10 * 60 * 1000);
}
@Override
protected void onDestroy() {
super.onDestroy();
// 没有移除回调,导致泄漏
// 正确的做法:leakyHandler.removeCallbacksAndMessages(null);
}
}
4.3 集成 LeakCanary 并触发泄漏
在 build.gradle(app 级别)中添加依赖:
dependencies {
// 其他依赖...
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:2.12'
}
运行应用,在 MainActivity 完全显示后,按返回键销毁 Activity。LeakCanary 会自动监听 Activity 生命周期,当检测到 Activity 实例未能被 GC 回收时,会在通知栏弹出“内存泄漏”通知,并自动生成 heap dump。
触发步骤:
- 安装并启动应用,进入 MainActivity。
- 按返回键或调用
finish()销毁 Activity。 - 等待约 5 秒(LeakCanary 默认延迟检测时间),观察通知栏提示。
- 点击通知打开 LeakCanary 的泄漏分析界面。
4.4 查看 Dump 结果:泄漏引用链分析
LeakCanary 会展示从 GC Roots 到泄漏对象的引用链。典型的泄漏链如下:
┬─ com.example.leakdemo.MainActivity
Leaking: YES (Activity#mDestroyed 为 true 且未 GC)
GC Root: 局部变量
├─ android.os.Handler (匿名内部类)
│ Leaking: YES (持有 Activity)
│ ↓ Handler.mCallback 为 null
└─ java.lang.ref.FinalizerReference
Leaking: UNKNOWN
↓ FinalizerReference.queue 为 null
实际 LeakCanary 会输出类似:
┬─ main chain
├─ com.example.leakdemo.MainActivity
│ Leaking: YES (Activity 已销毁)
│ GC Root: System Class (android.app.ActivityThread)
│ ├─ static ActivityThread.sCurrentActivityThread
│ │ └─ ActivityThread.mActivities
│ │ └─ ActivityClientRecord.activity
│ │ └─ MainActivity.leakyHandler
│ │ └─ Handler (匿名内部类)
│ │ Leaking: YES (持有 Activity)
│ │ └─ this$0 (指向 MainActivity)
其中 this$0 就是编译器为匿名内部类生成的指向外部类的引用,它就是泄漏的源头。
4.5 内联 SVG:Handler 匿名内部类泄漏流程
4.6 常见错误与调试技巧
| 常见错误 | 原因 | 解决方案 |
|---|---|---|
| 未集成 LeakCanary 时没有依赖 | 忘记添加 debugImplementation 依赖 | 检查 build.gradle 并同步 |
| 延迟时间过短,消息在 Activity 销毁前已被处理 | 消息被处理掉,Handler 不再持有引用 | 使用足够长的延迟(如10分钟)或 post 一个永不执行的消息 |
| 在 onDestroy 中执行了 removeCallbacksAndMessages | 移除了所有回调和消息,泄漏被修复 | 故意不调用 remove 来复现泄漏 |
| LeakCanary 通知不出现 | 后台进程被杀死或 App 的 debug 权限不足 | 确保 App 处于 debug 模式,且 LeakCanary 服务正常运行 |
4.7 性能与稳定性优化建议
- 避免延迟消息持有 Activity:使用静态内部类 +
WeakReference<Activity>,或在 onDestroy 中调用handler.removeCallbacksAndMessages(null)。 - 合理使用 Lifecycle:利用 LiveData 或协程在 Activity 销毁时自动取消任务。
- Dump 文件大小控制:LeakCanary 2.x 生成的内存快照通常较小(~1-5MB),但频繁泄漏可能导致多个 dump,建议在 CI 中接入 LeakCanary 的自动化检测。
- 避免生产环境引入:release 版本应使用
leakcanary-android-no-op空实现,防止性能损耗。
注意:本章用于教学演示,故意制造泄漏。实际项目中请勿在线上版本使用类似代码。务必在 onDestroy 中清除 Handler 所有消息,或者改用 Lifecycle-aware 组件。