Context 泄漏的典型现象:内存上升、OOM、页面退出后 GC 日志异常
1. 现象识别:从表象到异常信号
当 ViewModel 因错误持有 Activity Context 而导致泄漏时,最直观的表现是 内存持续上升、频繁 OOM 以及 页面退出后 GC 日志中出现大量不该存在的对象。这些现象并非孤立出现,而是相互印证、层层递进的异常链路。
核心认知:正常的 Android 页面退出后,Activity 及其关联的 ViewModel 应在几轮 GC 内完全回收。若退出后多次 GC 仍保留“Root Activity”或“ViewModel”的强引用路径,即可判定为 Context 泄漏。
| 现象 | 典型观测方式 | 严重程度 | 判断规则 |
|---|---|---|---|
| 内存上升 | Memory Profiler(Android Studio)实时曲线 | 中 | 连续操作页面 5 次后,常驻内存增量 > 1.5 MB 且不回落 |
| GC 日志异常 | Logcat 过滤 GC,观察 GC_FOR_ALLOC / GC_CONCURRENT 频率及 free / objects 变化 |
高 | 退出页面后 10 s 内仍出现以 Activity 或 ViewModel 为 root 的 AllocSpace 对象计数 |
| OOM 崩溃 | 崩溃日志的 OutOfMemoryError + 堆栈指向 ViewModel 内持有 Context 的代码行 |
致命 | 同一个页面(或多个同类型页面)重复触发 OutOfMemoryError: Failed to allocate,且堆栈与 ViewModel 内部引用相关 |
2. 数据采集:如何获取现场证据
在确认现象后,须立即采集以下三类数据来固定泄漏现场:
- 内存快照(Heap Dump): 触发页面退出后 5 s 内手动 dump(或通过 LeakCanary 自动抓取),保留
.hprof文件。 - GC 日志段: 开启
Logcat,过滤GC标签,定位到页面退出时间点前后的日志。 - LeakCanary 报告: 如已接入 LeakCanary,直接提取通知栏中的泄漏分析摘要。
采集命令参考: 在终端中通过 adb 持续抓取 GC 日志,以便事后精确分析。
adb logcat -s "GC" -v time | grep -E "GC_FOR_ALLOC|GC_CONCURRENT|ViewModel" > gc_leak.log
下面是一段典型的泄漏场景 Logcat 摘录(匿名化处理):
11-15 10:23:45.123 1234 5678 GC_FOR_ALLOC freed 128K, 5% free 2.1M/4.2M, paused 3ms, total 4ms
11-15 10:23:45.456 1234 5678 GC_CONCURRENT freed 34K, 4% free 2.6M/4.2M, paused 2ms, total 8ms
11-15 10:23:50.002 1234 5678 GC_FOR_ALLOC freed 512K, 12% free 4.8M/6.4M, paused 5ms, total 6ms
# 注意:持续 10s 后仍存在大量对象未被回收,且堆内存不下降
3. 原因定位:通过故障树反向溯源
故障树将复杂泄漏现象分解为可验证的判断节点,辅助每一步决策。
4. 排查判断规则:快速识别 Context 泄漏
依据故障树,采用以下 三步判断法 缩小范围:
- Check 1:退出页面后堆内存是否在 15 s 内持续增加? → 如“是”,视为可疑。
- Check 2:通过 heap dump 搜索
Activity实例数量是否 > 1?(正常情况下应为 0) → 如“是”,确认泄漏。 - Check 3:查看 ViewModel 类中是否包含
Context、View、Handler等字段? → 如“是”,定位到具体代码。
5. 修复动作:快速验证预方案
在确认泄漏现象后,可立即尝试替换为 Application Context 进行验证(无需完整重构):
- 将 ViewModel 构造中的
Context参数替换为application属性。 - 若 ViewModel 通过
AndroidViewModel(application)获取getApplication(),则无需额外保留 Context 引用。 - 避免在 ViewModel 内部创建
Handler或Runnable时传入 Activity 实例。
快速检查清单:
- ViewModel 是否直接保存了 Activity Context?
- ViewModel 是否通过
LiveData的观察者隐式持有 Activity? - ViewModel 内是否有匿名内部类(如
new Runnable)引用了外部 Activity?
6. 复测与预防:建立闭环
修复后,重新走一遍“现象识别”步骤,观察内存曲线是否在退出后平稳回落,GC 日志中不再出现异常对象计数。预防方面应坚持:
- 代码审查时重点标注 ViewModel 内的
Context字段; - 接入 LeakCanary 自动监测页面退出后的泄漏情况;
- 使用
AndroidViewModel代替自定义ViewModel来获取 Application Context。
经验基线: 退出一个标准详情页后,正常的堆内存应在 3 轮 GC 内恢复到操作前水平。任何超过 5 轮 GC 仍残留的
Activity 或其 ViewModel 实例,都应视为潜在泄漏。