1. 认识 ViewModel 持有 Activity 泄漏

现象识别

当 Activity 因配置变更(如屏幕旋转)被销毁重建时,旧 Activity 实例理应被 GC 回收。若此时大对象(如整个 Activity)仍被 ViewModel 对象“隐性”持有,便会导致内存泄漏。典型可见现象包括:

  • 界面卡顿:频繁旋转后,内存占用持续攀升,GC 触发频繁,UI 掉帧明显。
  • OOM 崩溃:连续操作数分钟后,出现 java.lang.OutOfMemoryError,Heap Dump 中可看到大量重复的 Activity 实例。
  • 生命周期错乱:ViewModel 中回调(如 LiveData/Flow)依然触发旧 Activity 的 UI 更新,导致空指针或状态显示不正确。

数据采集

快速确认泄漏需采集以下数据:

采集项工具 / 方法判断规则
Heap DumpAndroid Studio Profiler / MAT搜索目标 Activity 类名,若实例数超过 1 且存在 GC Root 路径指向 ViewModel,则泄漏。
内存计数adb shell dumpsys meminfo执行三次配置变更后,Activity 对象计数不下降,且 “Views” 相关大小未回落。
引用链LeakCanary 报告报告中明确出现 “leaking ViewModel” 或 “holder this” 到 Activity 的强引用路径。

原因定位

核心原因链:ViewModel 被 ViewModelStore 缓存,其内部字段(如回调接口、Context 引用、大型数据容器)直接或间接持有 Activity 实例的强引用,导致 Activity 无法在 onDestroy 后回收。

定位故障根因时,遵循以下排查树判断:

  1. 明确泄漏对象:Heap Dump 中 Activity 是否 > 1 个实例?
    • 是 → 进入第 2 步;否 → 非本场景泄漏。
  2. 找到 GC Root 路径:从 Activity 出发,反向查找所有强引用链。
    • 若路径中 最后一级持有者 是 ViewModel(或其内部类),则定位确认。
    • 否则检查路径中是否包含匿名内部类、Rx 订阅等。
  3. 判断持有类型:
    • 直接持有:ViewModel 内声明了一个指向 Activity 的强引用字段。
    • 间接持有:ViewModel 通过回调(listener)绑定到了 Activity 的匿名内部实例。

修复动作

针对最常见的 “ViewModel 持有 Activity 实例” 场景,采用分层修复策略:

层级修复措施示例代码片段
引用层将 Context / Activity 引用替换为 Application Context。
// 错误:直接持有 Activity
private val callback = object : MyCallback {
    override fun onResult(data: Any) {
        (activityRef?.get() as? MainActivity)?.updateUI(data)
    }
}

// 正确:使用 Application Context,不持有 Activity
private val appCtx = MyApplication.instance
private val callback = object : MyCallback {
    override fun onResult(data: Any) {
        appCtx?.let { /* 执行非 UI 操作 */ }
    }
}
生命周期层在 onCleared() 中主动清空所有回调 & 大数据引用。
override fun onCleared() {
    super.onCleared()
    // 清空所有监听器
    listener?.let { EventBus.unregister(it) }
    // 释放大型数据
    largeBitmap?.recycle()
    largeBitmap = null
    factory?.clear()
    factory = null
}
架构层ViewModel 内统一使用 WeakReference 包装 Context / Activity。
class MyViewModel : ViewModel() {
    private val activityRef = WeakReference(null)
    // 在 Fragment/Activity 创建时赋值
    fun attachActivity(act: Activity) { activityRef.set(act) }
    // 使用时通过 get() 获取,null 安全
    private fun doWork() {
        activityRef.get()?.let { act ->
            // 只能在此 lambda 内使用 act 对象
        }
    }
}

复测

  1. 自动化压力测试:编写测试用例,连续执行 20 次屏幕旋转,每次旋转后获取 Activity 实例计数。
  2. 阈值判断:旋转结束后总 Activity 实例数 ≤ 1,且内存占用量在 2 次 GC 后恢复至基线 ±5% 范围。
  3. LeakCanary 验证:运行测试 3 轮,LeakCanary 报告没有任何泄漏通知。

预防

架构级避坑规则:
• ViewModel 中不得直接引用 Activity / Fragment 实例;必须传递 Application Context。
• 回调(Listener / Observer)必须在 ViewModel.onCleared() 中主动解注册。
• 对于可序列化数据(如大图、长列表),使用 LiveData 隔层传递,避免 ViewModel 内部长期持有重型对象。
• 在项目 CI 中集成 LeakCanary 自动化检测,每次 MR 后自动触发泄漏扫描。
ViewModel 持有 Activity 泄漏流程 ViewModel 持有 Activity 泄漏:从正常到泄漏 Activity 创建 生命周期:onCreate → onStart ViewModel 创建 存储在 ViewModelStore 配置变更 / 销毁 onDestroy() 被调用 ViewModel 内强引用 字段 / 回调 / 匿名类持有 Activity 实例引用 GC 无法回收 Activity 仍存活于堆 内存泄漏持续叠加 每旋转一次累积一个 → 最终 OOM ⚠ 泄漏根因:ViewModel 生命周期长于 Activity ViewModel 属于 ViewModelStore(绑 Activity 生命周期),但内部强引 Activity → Activity 无法被 GC,每旋转一次累积一个,直到 OOM。