第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 数据采集:如何获取引用链证据?
- 抓取 heap dump(通过 Memory Profiler 或
adb shell am dumpheap) - 在 MAT / YourKit / Android Studio 中搜索目标 Activity 实例
- 查看 “
GC Root” 到该实例的最短强引用路径 - 重点关注路径中是否包含 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. 这些内部类是否持有
如果所有回答均为“是”,则根因锁定为 ViewModel 强引用链,修复方向为“改用弱引用或生命周期感知回调”。
1. 泄漏对象是否为 Activity 或 Fragment?
2. 引用链中的第一非 System 类是否为 ViewModel?
3. ViewModel 内部是否有匿名内部类、Lambda、或者非静态 Handler?
4. 这些内部类是否持有
this$0(对外部类的隐式强引用)?如果所有回答均为“是”,则根因锁定为 ViewModel 强引用链,修复方向为“改用弱引用或生命周期感知回调”。
4.4 修复动作:切断强引用链
- 将匿名内部类改为静态内部类,并通过构造函数显式传入 WeakReference<Activity>。
- 使用 Lifecycle 感知组件(如
LiveData、LifecycleEventObserver)确保回调在 Activity 销毁前自动移除。 - 对 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 存活?