1. 认识 ViewModel 持有 Activity 泄漏
现象识别
当 Activity 因配置变更(如屏幕旋转)被销毁重建时,旧 Activity 实例理应被 GC 回收。若此时大对象(如整个 Activity)仍被 ViewModel 对象“隐性”持有,便会导致内存泄漏。典型可见现象包括:
- 界面卡顿:频繁旋转后,内存占用持续攀升,GC 触发频繁,UI 掉帧明显。
- OOM 崩溃:连续操作数分钟后,出现 java.lang.OutOfMemoryError,Heap Dump 中可看到大量重复的 Activity 实例。
- 生命周期错乱:ViewModel 中回调(如 LiveData/Flow)依然触发旧 Activity 的 UI 更新,导致空指针或状态显示不正确。
数据采集
快速确认泄漏需采集以下数据:
| 采集项 | 工具 / 方法 | 判断规则 |
|---|---|---|
| Heap Dump | Android 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 后回收。
定位故障根因时,遵循以下排查树判断:
- 明确泄漏对象:Heap Dump 中 Activity 是否 > 1 个实例?
- 是 → 进入第 2 步;否 → 非本场景泄漏。
- 找到 GC Root 路径:从 Activity 出发,反向查找所有强引用链。
- 若路径中 最后一级持有者 是 ViewModel(或其内部类),则定位确认。
- 否则检查路径中是否包含匿名内部类、Rx 订阅等。
- 判断持有类型:
- 直接持有:ViewModel 内声明了一个指向 Activity 的强引用字段。
- 间接持有:ViewModel 通过回调(listener)绑定到了 Activity 的匿名内部实例。
修复动作
针对最常见的 “ViewModel 持有 Activity 实例” 场景,采用分层修复策略:
| 层级 | 修复措施 | 示例代码片段 |
|---|---|---|
| 引用层 | 将 Context / Activity 引用替换为 Application Context。 | |
| 生命周期层 | 在 onCleared() 中主动清空所有回调 & 大数据引用。 | |
| 架构层 | ViewModel 内统一使用 WeakReference 包装 Context / Activity。 | |
复测
- 自动化压力测试:编写测试用例,连续执行 20 次屏幕旋转,每次旋转后获取 Activity 实例计数。
- 阈值判断:旋转结束后总 Activity 实例数 ≤ 1,且内存占用量在 2 次 GC 后恢复至基线 ±5% 范围。
- LeakCanary 验证:运行测试 3 轮,LeakCanary 报告没有任何泄漏通知。
预防
架构级避坑规则:
• ViewModel 中不得直接引用 Activity / Fragment 实例;必须传递 Application Context。
• 回调(Listener / Observer)必须在 ViewModel.onCleared() 中主动解注册。
• 对于可序列化数据(如大图、长列表),使用 LiveData 隔层传递,避免 ViewModel 内部长期持有重型对象。
• 在项目 CI 中集成 LeakCanary 自动化检测,每次 MR 后自动触发泄漏扫描。
• ViewModel 中不得直接引用 Activity / Fragment 实例;必须传递 Application Context。
• 回调(Listener / Observer)必须在 ViewModel.onCleared() 中主动解注册。
• 对于可序列化数据(如大图、长列表),使用 LiveData 隔层传递,避免 ViewModel 内部长期持有重型对象。
• 在项目 CI 中集成 LeakCanary 自动化检测,每次 MR 后自动触发泄漏扫描。