2. ViewModel 生命周期与 Activity/Fragment 的关系:onCleared()、作用域、持有引用链
2.1 现象识别:泄漏的第一现场
当一个 Fragment 已经 onDestroy()、Activity 已经 onDestroyed(),但与其关联的 ViewModel 依然存活在内存中、并且其内部持有的 Context 对象(通常是 Activity 实例)无法被 GC 回收时,泄漏的第一个肉眼可见的现象就会出现在 内存快照 或 LeakCanary 通知 中。
- 旋转屏幕或切换 Fragment 后,App 内存只增不减。
- 重复进入同一页面,Memory Profiler 中 Activity 实例数比预期多 2+ 个。
- LeakCanary 上报
“Activity … has leaked”,且引用链中包含 ViewModel。
在本课程案例中,最典型的场景是:一个 ViewModel 通过构造参数或 ViewModelProvider.Factory 持有了当前 Activity 的引用,然后因为观察者/协程/回调没有被取消,导致 ViewModel 生命周期远超 Activity 生命周期。
2.2 数据采集:锁定引用链的“骨架”
数据采集阶段需要回答三个核心问题:ViewModel 什么时候该被清理?当前是否还在存活?是谁持有它?
| 采集项 | 方法 | 判断规则 |
|---|---|---|
| ViewModel 生存状态 | 在 ViewModel.onCleared() 内添加日志输出 | 日志应该出现一次;若出现在 Activity.onDestroy() 之前,说明 ViewModel 提前被清理,结构正常;若未出现,则 ViewModel 可能没有被释放 |
| Activity 实例存活数 | Android Profiler → Heap dump → 搜索 Activity 类名 | 应当只有 1 个(当前可见 Activity);出现 2+ 说明泄漏已发生 |
| 引用链深度 | LeakCanary 分析报告 / MAT 分析 | 最简引用链中不能包含 ViewModel 对象作为 GC Roots → Activity 的唯一路径 |
如果 ViewModel 中持有了
Activity context(或它的子类,如 Dialog、View),并且没有在 onCleared() 中置空,当 Activity 销毁而 ViewModel 仍然存活时,上下文对象就被“钉死”在内存中。
2.3 原因定位:ViewModel 生命周期与 Activity/Fragment 的生命周期关系
ViewModel 的生命周期设计遵循以下核心契约:
- 作用域:ViewModel 默认与创建它的
ViewModelStoreOwner(Activity/Fragment)绑定。当该 Owner 生命周期结束时(即onDestroyed()),ViewModel 会被ViewModelStore清理。 - onCleared() 调用时机:Owner 的
onDestroyed()之后、GC 回收之前。如果 ViewModel 在此时还持有外部资源(如 Context、网络请求、数据库引用),必须主动释放。 - 持有引用链:最常见的泄漏模式是
Activity.this → ViewModel (通过 constructor 注入) → 长期运行的 CoroutineJob → 外部回调 → 引用链不断,导致整个链无法回收。
下面是 ViewModel 生命周期与 Activity 生命周期的对比表格,也是判断泄漏的“基线”:
| Owner 状态 | ViewModel 状态 | onCleared() 调用? | 是否可安全持有 Context |
|---|---|---|---|
| create() → onStart() → onResume() | 已创建,活跃 | 否 | 可以(但优先用 Application Context) |
| onPause() → onStop() | 依然存活,不可见 | 否 | 可以,但应准备清理 |
| onDestroy()(非旋转重建) | 准备清理 | 是(紧接着调用) | 必须释放,否则泄漏 |
| onDestroy()(旋转重建) | 依然存活(因为 ViewModelStore 会被保留给新 Activity) | 否 | 仍然可以,但应使用弱引用或 Application Context |
根事件:Activity 实例泄漏
├── Activity 已经 onDestroy(),但 GC Root 仍可到达
│ ├── ViewModel 是 GC Root 路径上的中间节点
│ │ ├── ViewModel 通过构造函数直接持有 Activity.this
│ │ ├── ViewModel 通过 LiveData 持有 View 引用(View 又反引 Activity)
│ │ └── ViewModel 通过 resource callback 持有强引用
│ └── ViewModel 没有在 onCleared() 中释放这些引用
│ ├── 开发者不了解 onCleared() 的用途
│ └── 开发者误以为 ViewModel 会和 Activity 同时销毁
关键定位步骤:沿着 LeakCanary 的引用链往上找,如果看到类似 YourActivity → ViewModel → WeakReference? → AnotherObject,但没有弱引用的断开,那就是泄漏点。
2.4 修复动作:切断引用链
修复必须覆盖三个层面:
- 移除 Activity 的直接引用:使用
Application Context注入到 ViewModel 中,而非 Activity。 - 在 onCleared() 中切断所有外部订阅:调用
disposable.clear()、job.cancel()、removeObserver()。 - 对于必须持有 Activity 回调的场景:使用
SavedStateHandle或事件总线(如 LiveData/Flow)代替直接持有 Activity 实例。
// 错误方式:ViewModel 内部直接持有 Activity View 对象
class MyViewModel(private val someView: View) : ViewModel() {
init {
// someView 持有 Activity.this → 泄漏
}
}
// 正确方式:使用 LiveData/Flow + ViewModelProvider.Factory
class MyViewModel(private val application: Application) : AndroidViewModel(application) {
private val repository = MyRepository(application)
override fun onCleared() {
// 主动取消协程 / 清理观察者
repository.job.cancel()
}
}
同时,强烈建议在 ViewModel 的 onCleared() 中增加空指针保护并记录日志:
override fun onCleared() {
super.onCleared()
Log.d(TAG, "onCleared: 释放资源")
job?.cancel()
_liveData.value = null
// 将内部持有的 Context 置空(如果必须持有的话)
contextRef = null
}
2.5 复测:验证引用链已断开
1. 重复进入/退出页面 5 次 + 主动 GC。
2. Android Profiler → 检查 Activity 实例数是否稳定在 1。
3. LeakCanary 通知栏不再出现该页面泄漏提示。
4. 手动触发
onCleared() 日志,确认其在 Owner 销毁后确实被调用。
如果上述步骤全部通过,则泄漏修复成功;否则回到 数据采集 阶段,重新检查引用链。
2.6 预防:将生命周期契约内化到开发规范中
- 规范 1:ViewModel 的构造函数只接收
Application或SavedStateHandle,不从外部注入 Activity、Fragment、View。 - 规范 2:所有协程/回调/注册在
onCleared()中都必须有对应的cancel/disconnect动作,且按照“先清理子任务 → 清理观察者 → 置空持有”的顺序执行。 - 规范 3:开发阶段开启 LeakCanary,并为 ViewModel 的
onCleared()添加自动化测试(例如 Robolectric 模拟 destroy 场景)。
不要认为 ViewModel 与 Activity 同生共死。旋转屏幕时,旧的 Activity 销毁、新的 Activity 创建,但 ViewModel 会活下来。如果你的 ViewModel 持有的是旧 Activity 的引用,那就是一个静态泄漏点。
2.7 生命周期与泄漏关系图
上图揭示了三个要点:
- ViewModel 和 Activity 的生命周期错位(Activity 销毁后 ViewModel 可能继续存在);
- 泄漏引用链中,ViewModel 作为中间节点“钉住”了 Activity 实例;
- 修复的本质是在 onCleared() 中切断那条通往 Activity 的红色虚线。