在 ViewModel 中安全持有 Context:Application Context、AndroidViewModel、避免内存泄漏
1. 原理:为什么直接持有 Activity Context 可能导致内存泄漏?
ViewModel 的生命周期通常长于关联的 Activity/Fragment。当 ViewModel 直接持有 Activity Context 时,即使 Activity 已经被销毁,其引用依然被 ViewModel 持有,导致 Activity 对象无法被 GC 回收,引发 Activity 泄漏。
核心问题:ViewModel 存活于 ViewModelStoreOwner(如 Activity)的生命周期回调完成后才会被清除,但 Activity 的配置变更(如屏幕旋转)会使 ViewModel 仍然保留,而旧的 Activity 预期被销毁。直接持有 Activity Context 会阻止这一回收过程。
2. 安全方案:AndroidViewModel 与 Application Context
Android 提供了 AndroidViewModel 基类,它在构造时即可获取 Application 对象,持有的是全局唯一的 Application Context。Application Context 生命周期与进程一致,不会造成 Activity 泄漏。
| 方案 | Context 类型 | 生命周期 | 泄漏风险 |
|---|---|---|---|
| 直接持有 Activity Context | Activity | Activity 存活期 | 高 |
| 持有 Application Context | Application | 进程存活期 | 低 |
| 弱引用 + Activity Context | Activity(弱引用) | 随 GC 变化 | 中(需手动管理) |
| AndroidViewModel | Application(构造函数注入) | 进程存活期 | 低(推荐) |
3. 实现流程
- 新建 ViewModel 类继承
AndroidViewModel。 - 在构造函数中通过
application参数获取Application对象。 - 将需要 Context 的操作限制在 Application 范围内(如资源获取、SharedPreferences、系统服务)。
- 避免在 ViewModel 中保存 Activity/Fragment 引用或 View 引用。
4. 代码示例
// 安全的 ViewModel 实现
public class MyViewModel extends AndroidViewModel {
private final Application app;
public MyViewModel(@NonNull Application application) {
super(application);
this.app = application;
}
// 安全使用 Application Context
public String getAppVersionName() {
try {
return app.getPackageManager()
.getPackageInfo(app.getPackageName(), 0).versionName;
} catch (PackageManager.NameNotFoundException e) {
return "unknown";
}
}
// 避免使用 Activity Context 做 UI 操作
// 错误做法:直接持有 Activity 并调用 runOnUiThread
}
5. 调试:检测内存泄漏
推荐集成 LeakCanary 自动检测 Activity 泄漏:
// build.gradle 依赖
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.12'
// 无需额外初始化,安装后自动监控 Activity、Fragment、ViewModel
// 泄漏发生时会在通知栏显示,并给出引用链
典型泄漏路径:Activity → ViewModel → Activity Context → Activity 形成循环引用。
6. 常见错误
- 直接赋值 Activity 给 ViewModel 字段:
myViewModel.setContext(activity); - 在 ViewModel 中持有 View 引用:
viewModel.button = findViewById(...); - 匿名内部类/回调持有外部类引用:在 ViewModel 中注册监听器时隐式持有 Activity。
- 使用静态字段存储 Activity:静态变量生命周期等于进程,更严重。
即使使用了 Application Context,仍然不建议在 ViewModel 中执行大量 I/O 或耗时操作,应该将具体工作委托给 Repository 或协程作用域。
7. 性能与稳定性优化
- 唯一性:Application 在进程中只有一个实例,使用 AndroidViewModel 可避免重复创建。
- 资源访问:通过 Application Context 获取 Resources、SharedPreferences 是安全的,因为这些资源属于进程级别。
- 跨屏幕旋转:配置变更后 ViewModel 保留,Application Context 不变,不需要额外处理。
- 替代方案:若必须持有短期 Context,使用
WeakReference<Context>并确保在 onCleared() 中清除回调。
8. 安全持有 Context 结构示意
9. 总结原则
- 优先使用
AndroidViewModel(继承而非直接持有 Context)。 - 如果必须使用自定义 Context(如系统服务),也请通过
Application.getSystemService()获取。 - 不要在 ViewModel 中保存
Activity、View、Fragment或Context的非 Application 实例。 - 配合 LeakCanary 在开发阶段持续监控,尽早发现泄漏。