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. 原因定位:通过故障树反向溯源

故障树将复杂泄漏现象分解为可验证的判断节点,辅助每一步决策。

Context 泄漏现象故障树 页面退出后内存异常 内存曲线持续上升 GC 日志高频 & 无法回收 OOM 崩溃反复出现 Activity 强引用未被清除 ViewModel 内持 mContext 匿名内部类隐式持有 直接持有 Activity Context 持有 View / Dialog 等强引用 ✅ 根本原因:ViewModel 未使用 Application Context 故障树判据 沿红线走到叶子节点

4. 排查判断规则:快速识别 Context 泄漏

依据故障树,采用以下 三步判断法 缩小范围:

  1. Check 1:退出页面后堆内存是否在 15 s 内持续增加? → 如“是”,视为可疑。
  2. Check 2:通过 heap dump 搜索 Activity 实例数量是否 > 1?(正常情况下应为 0) → 如“是”,确认泄漏。
  3. 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 实例,都应视为潜在泄漏。