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
故障树分析(Fault Tree):为什么 ViewModel 泄漏了 Activity 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 修复动作:切断引用链

修复必须覆盖三个层面:

  1. 移除 Activity 的直接引用:使用 Application Context 注入到 ViewModel 中,而非 Activity。
  2. 在 onCleared() 中切断所有外部订阅:调用 disposable.clear()、job.cancel()、removeObserver()。
  3. 对于必须持有 Activity 回调的场景:使用 SavedStateHandle 或事件总线(如 LiveData/Flow)代替直接持有 Activity 实例。
实验案例:一个因 LiveData 使用不当导致的泄漏修复
// 错误方式: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/Fragment 的关系 + 泄漏引用链 Activity 生命周期 onCreate() onResume() onPause() onDestroy() (可以回收) 生命 周期 绑定 ViewModel 生命周期 ViewModel 创建 ViewModel 存活 (跨旋转存活) onCleared() 释放资源 实例可回收 泄漏引用链示意 Activity.this (GC Root) ViewModel (持有引用) 未取消的 Coroutine Activity 无法回收 🔍 判断规则(故障树) 1. 如果「Activity.onDestroy()」已执行,但「ViewModel.onCleared()」未执行 → ViewModel 还活着吗?→ 检查 ViewModelStoreOwner 是否被释放 2. 如果「onCleared()」已执行,但引用链中仍然有 Activity → ViewModel 内部清空不干净 → 排查 LiveData/Observer 是否被移除 3. 修复标准:引用链不能包含「Activity ← ViewModel」这条实线路径。

上图揭示了三个要点:
- ViewModel 和 Activity 的生命周期错位(Activity 销毁后 ViewModel 可能继续存在);
- 泄漏引用链中,ViewModel 作为中间节点“钉住”了 Activity 实例;
- 修复的本质是在 onCleared() 中切断那条通往 Activity 的红色虚线。