4. ViewModel 泄漏的典型特征:Activity/Fragment 被 ViewModel 长生命周期持有
当 Activity / Fragment 因配置变更重建时,旧实例本应被 GC 回收,却因为被 ViewModel 中的某个长生命周期对象(如 LiveData 观察者、协程 Job、回调、有界 View 引用)牢牢持有,导致内存中残留大量“僵尸” Activity / Fragment。本章通过故障树、排查表、判断规则,系统拆解这一特征。
4.1 现象识别 — 故障树
故障树根节点: 配置变更后内存持续增长,来回切换 3 次以上,Heap dump 中旧 Activity 实例数 > 当前栈内 Activity 数。
- 叶节点 1: LeakCanary 报告
Activity instance leaked,引用链尾部直指某个 ViewModel。 - 叶节点 2: 手动触发 GC 后,在 profiler 中仍能看到存活的前一 Activity 对象。
- 叶节点 3: 同一个 Fragment 被重复创建,但旧 Fragment 的
onDestroy()日志未打出。
快速判断规则:
- 规则 A:旋转屏幕后,
Runtime.totalMemory()上升量 > 当前 Activity 简单视图树的理论大小 × 2 → 很可能有实例残留。 - 规则 B:用
adb shell dumpsys meminfo <package>查看 Activities 计数,如果 > 1 且与旋转次数成正比 → 存在 ViewModel 持有泄漏。
4.2 数据采集 — 持有关键证据
采集以下证据,用于后续原因定位:
| 证据类型 | 采集方法 | 关键看什么 |
|---|---|---|
| Heap dump | Android Studio Profiler → Dump Java Heap | 查找 Activity/Fragment 对象,检查其 GC Root 路径。 |
| LeakCanary 报告 | 集成 LeakCanary,触发泄漏后读取通知 | 引用链倒数第二或第三节点是否为 ViewModel 实例。 |
| Logcat 生命周期日志 | 在 Activity/Fragment 的 onDestroy() 中打 Log.d(TAG, "onDestroy") | 对比重建次数与 onDestroy 打印次数是否一致。 |
| Mat / PerfMate 分析 | MAT 打开 hprof,用 OQL 查 select * from instanceof android.app.Activity | 统计数量,看是否有多个不属于当前栈的 Activity。 |
4.3 原因定位 — 定位持有链
典型的“ViewModel → 长生命周期对象 → Activity/Fragment”持有链条有以下三种模式:
- LiveData 观察者未移除: ViewModel 中持有
LiveData.observe(lifecycleOwner),但传入的lifecycleOwner是一个 View 或旧 Activity,而 ViewModel 的onCleared()未取消注册。 - 协程 Job 隐式捕获: ViewModel 内启动
viewModelScope.launch,但在协程内部使用了requireActivity()或view的强引用。 - 回调/监听器注册: ViewModel 将 Activity / View 注册为回调(如
SystemService、SensorManager),未在onCleared()中注销。
· 易混淆点: 很多时候泄漏表面看是静态变量或单例导致,但实际根源是 ViewModel 内部将这些静态引用委托给了 Activity 实例。务必检查 ViewModel 中的所有成员变量和匿名内部类。
引用链分析表:
| GC Root 路径片断 | 常见泄漏源 | 判断规则 |
|---|---|---|
ViewModel → LiveData → Observer → Activity | Observer 以 lambda/anonymous class 形式持有外部 Activity | Observer 内部是否使用了 activity 或 context? |
ViewModel → Job → Continuation → Fragment | 协程挂起函数中保留了 Fragment 引用 | 搜索协程块内是否出现 requireFragment() 或 view。 |
ViewModel → Listener → Activity | ViewModel 中 eventBus.register(activity) 但未 unregister | 查看 onCleared() 中是否有对应的注销操作。 |
4.4 修复动作 — 中断持有链
根据定位结果,执行以下修复:
- LiveData 场景: 改用
viewLifecycleOwner或observe(lifecycleOwner)后确保 listener 在 LifecycleOwner 销毁时自动移除;若手动注册,在onCleared()中调用liveData.removeObserver(observer)。 - 协程场景: 在 ViewModel 的
onCleared()中取消viewModelScope.coroutineContext[Job]?.children?.forEach { it.cancel() };确保协程内部避免直接使用activity或fragment强引用,改为传入独立的callback接口或使用WeakReference。 - 回调场景: 在
onCleared()中统一注销所有注册的监听器;推荐使用LifecycleObserver配合lifecycle.addObserver()自动管理生命周期。
修复示例: 清理协程残留的 ViewModel 代码片段
class MyViewModel : ViewModel() {
private var job: Job? = null
fun startListening() {
job = viewModelScope.launch {
// 避免使用强引用,改用回调方式传递结果
val result = repository.fetchData()
callback?.onResult(result) // callback 应为 WeakReference<ResponseHandler>
}
}
override fun onCleared() {
super.onCleared()
job?.cancel() // 主动取消协程
// 若存在 LiveData observer,在此处移除
myLiveData.removeObserver(observer)
}
}
4.5 复测 — 验证修复有效性
执行以下复测步骤:
- 重现代码:连续快速旋转屏幕 5 次,每次间隔 500ms。
- 抓取 Heap dump(After GC)查看 Activity 实例总数,应 ≤ 1(当前前台 Activity)。
- 检查 Logcat:每旋转一次都应看到
onDestroy()日志,且onCreate()次数与旋转次数相同。 - 使用 LeakCanary 确认为 No Leak 状态。
· 陷阱: 某些 ViewModel 中持有 SavedStateHandle 时,即使 ViewModel 被清除,SavedStateHandle 可能仍然持有旧 Activity 的 Bundle 引用。需检查 SavedStateHandle 内是否存储了自定义 Parcelable 对象,这些对象中是否又持有 Context。
4.6 预防 — 设计时避免长生命周期持有
| 预防措施 | 做法 | 原因 |
|---|---|---|
| 禁止强引用 View | ViewModel 内使用 LiveData<T> 传递数据,绝不用 View 类型字段 | View 持有 Activity 引用链 |
优先使用 observer(lifecycleOwner) | 传入 viewLifecycleOwner 而非 activity | 生命周期自动管理,避免手动清理遗漏 |
| 协程中传入接口 | 定义 interface ResponseCallback,ViewModel 持有 WeakReference<ResponseCallback> | 避免隐式捕获 Activity |
| 统一注册管理 | 创建 LifecycleObserver 并在 onCreate 中注册 | ViewModel.onCleared 不再是唯一清理入口 |
| 代码审查规则 | 禁止在 ViewModel 中添加 Activity、Fragment、View、Context 类型字段 | 从根本上切断持有链 |
最佳实践: 在团队 GitLab MR 模板中要求“ViewModel 新增字段不得包含 android.app.Activity / android.support.v4.app.Fragment / android.view.View 类型”,并配置 SpotBugs 或 Detekt 规则自动检查。