第2章 内存泄漏根本原因:强引用未释放、生命周期管理错误、未注册回调、静态集合、非静态内部类

2.1 现象识别:直观感受与性能拐点

内存泄漏并非一蹴而就,它具有累积性。早期可能只是轻微的卡顿,随着泄漏对象增多,GC频率上升,最终导致OOM。下表列出了不同阶段的典型现象,用于快速诊断是否进入泄漏排查流程。

现象类型可感知指标工具验证指标严重等级
轻度应用切换时偶现白屏、列表滚动掉帧Profiler 中 Java Heap 持续增长但手动 GC 后下降不足 30%★☆☆☆☆
中度退出页面后内存占用未回落、应用被系统频繁回收Heap Dump 后 MAT 显示多个 Retained Set > 5 MB★★★☆☆
重度操作特定功能后闪退、Logcat 输出连续 GC 且 Finalizer Watchdog 超时LeakCanary 自动触发分页堆栈,且 WeakReference 始终无法被回收★★★★★
判断规则: 当你发现 adb shell dumpsys meminfo 中某 Activity 或 Fragment 在 destroy 后 mBase 引用仍然存在,说明已泄漏,直接进入下一阶段的数据采集。

2.2 数据采集:锁定强引用链

在这一步中,我们需要收集两个维度的数据:

  • 自动化快照:集成 LeakCanary 库,当疑似泄漏发生时自动触发 dumpHprofData()。
  • 手动对照:在关键业务入口和出口分别执行一次 adb shell am start -W com.xxx/.Activity 操作,对比多次 Heap Dump。

下图为五种根本原因及其触发的排查流程,请先通过这张故障树建立全局印象,后续章节将逐一展开。

内存泄漏根本原因决策树 泄漏入口确认 强引用未释放 生命周期管理错误 未注册回调 静态集合 引用了 Context Activity销毁后仍有引用 匿名内部类做回调 List/Map静态持有 单例 + Activity.this Handler/Thread 未取消 观察者模式未 remove 缓存、历史页面集合 排查顺序:先检查静态集合 → 强引用链 → 回调 → 内部类 → 生命周期 LeakCanary 覆盖已知模式,MAT 用 Dominator Tree 验证引用路径

2.3 原因定位:五类根本原因的比对排查表

当数据采集完成后,使用表格中的判断规则迅速缩小范围,这是避免在错误方向上浪费时间的关键。

根本原因入口条件嫌疑区域判断规则典型案例
强引用未释放 长生命周期对象持有短生命周期对象的直接引用 单例、Application 级缓存、系统 Service 绑定 MAT 中 Path to GC Roots → exclude phantom/weak → 看到强引用链 自定义 XXXManager 中持有 Activity.this
生命周期管理错误 对象在 onDestroy/onStop 之后仍有活跃引用 Handler、Thread、AsyncTask、协程 Job LeakCanary 报告 Fragment 泄漏,堆栈显示 Handler.postDelayed 持有 Activity 页面关闭后 Handler 延迟消息仍在 MessageQueue 中
未注册回调 注册了监听器/观察者/广播,但未在生命周期结束时注销 registerReceiver、addObserver、EventBus、LiveData 泄露堆栈指向某个回调接口实现,且该接口注册于 onCreate/onStart EventBus.register(this) 缺少 unregister
静态集合 static List/Map/Set 随着业务增长不断 add 对象 全局缓存池、历史记录栈、正在下载列表 MAT 中看到某个 ArrayList 拥有大量 Retained 对象,且来自 static 字段 static List<Activity> 未在 destroy 时 remove
非静态内部类 非静态的匿名内部类或命名的内部类持有外部类实例 匿名 Runnable、自定义 View、内部类 Listener MAT 显示 this$0 或 outerClass 字段指向已泄露的 Activity new Runnable() 或 class MyTask 中隐式持有 Activity
避坑要点: 许多新手排查时误用 Profiler 的“Live Count”判断泄漏,这只能给出存活对象数。正确做法是“手动强制 GC → 导出 Heap”对比两次的快照,如果某 Activity 数量在 destroy 后还大于 0,才锁定为泄漏。

2.4 修复动作:针对五种原因的 SOP

根因不同,修复手段差异很大。以下按优先级给出标准化操作流程。

2.4.1 消除强引用未释放

  • 将强引用改为 WeakReference<T> 或 SoftReference<T>,尤其是单例持有的 Context。
  • 在长生命周期对象中提供 release() 或 clear() 方法,由短生命周期对象显式调用。
// 错误写法
public class AppManager {
    private static AppManager instance;
    private Activity currentActivity;  // 强引用导致泄漏
    // ...
}

// 修复写法
public class AppManager {
    private static AppManager instance;
    private WeakReference<Activity> currentActivityRef;
    public void setCurrentActivity(Activity activity) {
        this.currentActivityRef = new WeakReference<>(activity);
    }
    public Activity getCurrentActivity() {
        return currentActivityRef != null ? currentActivityRef.get() : null;
    }
}

2.4.2 生命周期管理修正

  • 在 onDestroy() 中调用 handler.removeCallbacksAndMessages(null)。
  • 在 Kotlin 协程中使用 viewModelScope 或 lifecycleScope,避免自定义生命周期。
  • Thread 或 AsyncTask 应并入 Lifecycle 观察,使用 getLifecycle().addObserver() 控制存活周期。
// 典型修复:清理 Handler
@Override
protected void onDestroy() {
    super.onDestroy();
    // 移除所有未处理的消息,防止延迟消息持有 Activity
    if (myHandler != null) {
        myHandler.removeCallbacksAndMessages(null);
    }
    // 对于 AsyncTask,立即取消
    if (myTask != null && myTask.getStatus() != AsyncTask.Status.FINISHED) {
        myTask.cancel(true);
    }
}

2.4.3 未注册回调修复

  • 明确配对注册/反注册:onCreate 对应 onDestroy,onResume 对应 onPause。
  • 使用 LifecycleEventObserver 在 DESTROY 时自动清除回调,或使用 AutoDispose/RxLifecycle。
// 配对注册写法
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    EventBus.getDefault().register(this);
}

@Override
public void onDestroy() {
    super.onDestroy();
    EventBus.getDefault().unregister(this);
}

2.4.4 静态集合清理

  • 静态集合使用 WeakHashMap<K, WeakReference<V>> 或 LRUCache + 软引用。
  • 每次 add 对象时,按业务生命周期设置 Remove 回调或使用 Lifecycle 观察。
// 使用 WeakHashMap 避免静态集合强引用泄漏
static Map<String, WeakReference<Activity>> sActivityCache = new WeakHashMap<>();
// 添加时
sActivityCache.put(tag, new WeakReference<>(activity));
// 获取时
Activity act = sActivityCache.get(tag).get();

2.4.5 非静态内部类转静态

  • 将内部类定义为 static class,使其不持有外部类引用。
  • 如果必须持有外部引用,对外部类使用 WeakReference 显式传入。
// 错误:非静态匿名内部类
postDelayed(new Runnable() {
    @Override
    public void run() {  // 隐式持有 Activity
        updateUI();  
    }
}, 5000);

// 修复:改为静态内部类
static class MyRunnable implements Runnable {
    private WeakReference<Activity> activityRef;
    MyRunnable(Activity activity) {
        this.activityRef = new WeakReference<>(activity);
    }
    @Override
    public void run() {
        Activity act = activityRef.get();
        if (act != null) {
            act.updateUI();
        }
    }
}

// 或者直接使用 Lambda + 静态方法引用(需确保上下文不泄漏)

2.5 复测:验证方案

执行“进入页面 → 触发泄漏操作 → 反复退出/进入 5 次 → 手动 GC → 导出 Heap”的循环,使用 LeakCanary 自检进行回归对比。若 LeakCanary 未触发任何 Notification,且 MAT 上对应 Activity 实例数为 0,则判定修复通过。

复测判断标准:
  1. LeakCanary 连续测试 3 次无泄漏告警。
  2. MAT 上 Show Largest Retained Set 为 0,或目标 Activity 的 Retained Heap < 1KB。
  3. 单次内存操作(如跳转 100 次页面后)总内存涨幅不超过 2MB。

2.6 预防:编码规范与自动化

在修复一轮后,团队应建立以下机制避免同类问题重复发生:

  • 编译期检查:使用 Lint 规则或自定义 Pitest/SpotBugs 检测非静态内部类、未注销的 BroadcastReceiver。
  • 编码规范:禁止非静态内部类持有 Activity;单例必须通过 WeakReference 获取 Context。
  • 静态代码分析:将 LeakCananry 引入 CI 流程,merge 前若出现任何泄漏 notification,则终止构建。
  • 测试卡点:在 UI 自动化测试脚本中加入页面跳转后的内存快照对比,阈值设为 +3MB。

至此,第 2 章已完成了从现象识别、数据采集、原因定位、修复动作、复测到预防的全部排查链路,接下来的章节将以具体场景驱动,逐一深入到 LeakCanary 和 MAT 的联合操作细节。