在 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 ContextActivityActivity 存活期高
持有 Application ContextApplication进程存活期低
弱引用 + Activity ContextActivity(弱引用)随 GC 变化中(需手动管理)
AndroidViewModelApplication(构造函数注入)进程存活期低(推荐)

3. 实现流程

  1. 新建 ViewModel 类继承 AndroidViewModel。
  2. 在构造函数中通过 application 参数获取 Application 对象。
  3. 将需要 Context 的操作限制在 Application 范围内(如资源获取、SharedPreferences、系统服务)。
  4. 避免在 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 结构示意

ViewModel 安全持有 Context 的两种路径对比 Activity ViewModel 持有 Activity Context Activity 泄漏 无法被 GC 回收 Activity AndroidViewModel Application Context 全局唯一,进程存活 安全无泄漏 资源/持久化操作 关键要点 • 左侧路径(红)直接持有 Activity Context → 导致 Activity 泄漏 • 右侧路径(绿)通过 AndroidViewModel 获取 Application Context → 安全可靠

9. 总结原则

  • 优先使用 AndroidViewModel(继承而非直接持有 Context)。
  • 如果必须使用自定义 Context(如系统服务),也请通过 Application.getSystemService() 获取。
  • 不要在 ViewModel 中保存 Activity、View、Fragment 或 Context 的非 Application 实例。
  • 配合 LeakCanary 在开发阶段持续监控,尽早发现泄漏。