第4章 泄漏根因分析框架:引用链分类、强引用与软引用、GC Root概念

4.1 现象识别:什么类型的泄漏能用这套框架分析?

在 ViewModel 泄漏 Activity 的典型故障中,现象通常表现为:

  • 旋转屏幕后,Activity 实例数量不减少(adb shell dumpsys activity activities | grep "ActivityRecord" 显示多个副本)
  • Memory Profiler 中 Activity 对象无法被 GC 回收,即使返回桌面后仍处于 “可到达” 状态
  • heap dump 分析 中发现 Activity 对象有一条从 ViewModel 对象经匿名内部类(或回调)强引用到达的引用链
  • LeakCanary 报告 明确指出:“Activity instance retained by ViewModel → inner listener → this$0”

这些现象的根因都属于 “强引用阻断 GC” 模型。本章建立的框架正是为这类场景提供统一分析入口。

4.2 数据采集:如何获取引用链证据?

  1. 抓取 heap dump(通过 Memory Profiler 或 adb shell am dumpheap)
  2. 在 MAT / YourKit / Android Studio 中搜索目标 Activity 实例
  3. 查看 “GC Root” 到该实例的最短强引用路径
  4. 重点关注路径中是否包含 ViewModel、匿名内部类、非静态 Handler、Runnable 等持有 this$0 引用的节点
判断规则表: 根据引用链末端类型快速定位泄漏类型
引用链末端节点类型 常见泄漏模式 典型修复策略
Activity.this$0(内部类) ViewModel 中的匿名内部类持有外部 Activity 改为静态内部类 + WeakReference
MessageQueue 中的 Message Handler 未移除回调 onDestroy 时 removeCallbacks
Thread/Runnable 对象 后台线程持有 Activity View 取消线程或使用弱引用
ViewModel 中的 LiveData Observer Observer 未在 Activity 生命周期结束时自动移除 使用 Lifecycle 感知或手动移除

4.3 原因定位:引用链分类 & GC Root 解析

GC Root 是垃圾回收器判定对象存活的起点。以下为 Android 中常见的 GC Root 类型:

  • 活动线程(Thread 栈帧局部变量)
  • 静态变量(Class 常量、静态字段)
  • JNI 全局引用
  • Monitor 占用对象(synchronized)
  • 系统类加载器

在 ViewModel 泄漏 Activity 的场景中,常见的 GC Root 是“活动线程”或“静态变量”。例如 ViewModel 内启动一个线程执行网络请求,线程持有回调接口,回调接口(匿名内部类)持有外部 Activity —— 这条引用链的根就是 “active thread”。

引用链按引用强度分为三类(仅列出与泄漏分析强相关的):

引用类型 GC 判定规则 泄漏角色
强引用(Strong Reference) 只要有强引用链到达 GC Root,对象绝不回收 泄漏元凶
软引用(Soft Reference) 内存不足时回收 不直接导致泄漏
弱引用(WeakReference) 下次 GC 立即回收(若只被弱引用到达) 修复工具

分析泄漏的黄金法则:
找出从 GC Root 到泄漏对象之间的所有强引用,将其中任一强引用替换为弱引用,即可切断泄漏链。

故障树判断规则(用于快速诊断 ViewModel 泄漏):
1. 泄漏对象是否为 Activity 或 Fragment?
2. 引用链中的第一非 System 类是否为 ViewModel?
3. ViewModel 内部是否有匿名内部类、Lambda、或者非静态 Handler?
4. 这些内部类是否持有 this$0(对外部类的隐式强引用)?
如果所有回答均为“是”,则根因锁定为 ViewModel 强引用链,修复方向为“改用弱引用或生命周期感知回调”。

4.4 修复动作:切断强引用链

  1. 将匿名内部类改为静态内部类,并通过构造函数显式传入 WeakReference<Activity>。
  2. 使用 Lifecycle 感知组件(如 LiveData、LifecycleEventObserver)确保回调在 Activity 销毁前自动移除。
  3. 对 ViewModel 中可能持有 View 引用的字段,用 WeakReference 包装。
// 修复前(强引用链)
public class MyViewModel extends ViewModel {
    private final Listener listener = new Listener() {
        @Override
        public void onEvent() {
            // 隐式持有外部 Activity(this$0)
            activity.updateUI();
        }
    };
}

// 修复后(弱引用链)
public class MyViewModel extends ViewModel {
    private final WeakReference<Activity> activityRef;
    private final StaticListener listener = new StaticListener(activityRef);
    
    private static class StaticListener implements Listener {
        private final WeakReference<Activity> activityRef;
        StaticListener(WeakReference<Activity> ref) { this.activityRef = ref; }
        @Override
        public void onEvent() {
            Activity activity = activityRef.get();
            if (activity != null) activity.updateUI();
        }
    }
}

4.5 复测验证

  • 手动触发 GC: 在 Android Studio 中点击 “Initiate GC”,确认 Activity 实例被回收。
  • LeakCanary 无告警: 来回旋转屏幕 10 次,LeakCanary 不应弹出泄漏通知。
  • Dump 检查: 抓取 heap dump 后搜索目标 Activity 类名,数量应为 1(当前可见的)或 0(已销毁的)。
复测通过标准矩阵:
检查项 通过条件 检查工具
Activity 实例计数 旋转后不超过当前可见数 + 1 Memory Profiler / dump
GC 日志 无 “waiting to GC” 阻塞 logcat
LeakCanary 无泄漏报告 LeakCanary 通知栏

4.6 预防:编码规范

  • ViewModel 中永远不要持有 Activity / Fragment / View 的强引用。
  • 使用 SavedStateHandle 替代靠 ViewModel 字段恢复 UI 状态。
  • 内部类 / Lambda 如果涉及 UI 操作,必须判断 getLifecycle().getCurrentState() 是否可用。
  • 代码审查时,使用 “泄漏模式检查清单”:
泄漏模式检查清单(编码阶段):
1. 是否有非静态内部类持有外部类引用(this$0)?
2. ViewModel 中是否有 Runnable / Callback 类型的字段?
3. 是否在线程中直接使用 Activity 对象?
4. 是否在 onDestroy 后仍有 Observer 存活?
ViewModel 泄漏 GC Root 引用链分析 ViewModel 泄漏引用链 – 从 GC Root 到 Activity GC Root (活动线程) 最强引用 → ViewModel 实例 匿名内部类 (持有 this$0 强引用) Activity (泄漏对象) 关键判断 每条深色线都是 强引用,阻断 GC 替换任意一条为 弱引用即可修复 修复动作 改为静态内部类 + WeakReference 切断 this$0 引用链分类:GC Root → 强引用(ViewModel) → 强引用(内部类) → 强引用(Activity) → 泄漏