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

触发步骤:
  1. 安装并启动应用,进入 MainActivity。
  2. 按返回键或调用 finish() 销毁 Activity。
  3. 等待约 5 秒(LeakCanary 默认延迟检测时间),观察通知栏提示。
  4. 点击通知打开 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 匿名内部类泄漏流程

Handler匿名内部类泄漏流程 Handler匿名内部类内存泄漏流程 Activity Handler匿名内部类 (隐式持有Activity引用) 创建并持有 MessageQueue sendEmptyMessageDelayed Activity onDestroy 用户返回销毁 GC 无法回收 Handler持有引用 LeakCanary 检测到泄漏,生成Dump 5秒后检测

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 组件。