第2章 理解ViewModel生命周期:创建、清除、onCleared与测试模拟

2.1 ViewModel生命周期原理

ViewModel的生命周期独立于Activity/Fragment的旋转配置变化,但受ViewModelStoreOwner(如Activity、Fragment)的最终销毁影响。其核心生命周期阶段包括:创建(构造)、活跃使用、清除(onCleared)。

  • 创建:首次通过 ViewModelProvider 获取时,ViewModel被实例化并绑定到ViewModelStore。
  • 活跃使用:在Owner存活期间,ViewModel持有数据,可响应状态变化。
  • 清除:当Owner的 onDestroy() 被调用且不是因配置变更引起时,ViewModelStore.clear() 触发,依次调用每个ViewModel的 onCleared()。
关键区别:配置变更(如屏幕旋转)不会导致ViewModel清除;只有Activity.finish()或Fragment被移除等最终销毁才会触发清除。
ViewModel生命周期与测试模拟流程 Owner创建ViewModel ViewModel初始化 Owner存活期间 Owner最终销毁 onCleared()被调用 测试中手动触发清除 验证资源释放/清理 配置变更不清除 需手动模拟

2.2 实现流程:从创建到清除

  1. Owner(Activity/Fragment)启动,首次调用 ViewModelProvider.get(MyViewModel::class.java)。
  2. ViewModelStore.newInstance()创建MyViewModel实例,存入Map。
  3. Owner存活期间,ViewModel可以持有LiveData、Flow或协程作用域。
  4. 当Owner因finish()/remove()导致最终销毁时,ViewModelStore.clear()遍历所有ViewModel实例。
  5. 每个ViewModel的 onCleared() 被调用,释放资源(关闭数据库、取消协程等)。
  6. Owner被回收,ViewModel对象变为不可达。

2.3 API与关键方法

方法/接口作用调用时机
onCleared()清理资源、取消订阅ViewModelStore.clear() 时,Owner最终销毁后
ViewModelStore.clear()清除所有ViewModel并触发onClearedActivity.onDestroy() 非旋转时 / Fragment.onDestroy()
ViewModelProvider.get()创建或返回已存在的ViewModel首次访问或配置变更后重新关联
closeable(继承自AutoCloseable)自动在onCleared时关闭Closeable对象onCleared 内部调用

2.4 测试中模拟生命周期的关键方法

单元测试中无法依赖Android组件生命周期,需手动模拟ViewModel的创建与清除。以下是三种常见策略:

2.4.1 直接构造并手动调用onCleared

适合纯Kotlin/Java ViewModel,无需依赖Android框架。

class MyViewModelTest {
    private lateinit var viewModel: MyViewModel

    @Before
    fun setUp() {
        viewModel = MyViewModel(/* 依赖注入 */)
    }

    @Test
    fun testOnClearedCancelsJob() {
        // 模拟开始使用
        viewModel.startSomeWork()
        // 手动触发清除
        viewModel.onCleared()
        // 验证协程已被取消
        assertTrue(viewModel.scope.isCancelled)
    }
}

2.4.2 使用TestCoroutineDispatcher验证协程取消

当ViewModel内部使用 viewModelScope 时,可控制调度器。

@Test
fun testCoroutineCancellationOnCleared() = runTest {
    val testDispatcher = StandardTestDispatcher()
    Dispatchers.setMain(testDispatcher)
    val viewModel = MyViewModel()
    viewModel.doWork()
    // 模拟owner销毁
    viewModel.onCleared()
    // 推进时间,确保取消传播
    testDispatcher.scheduler.advanceUntilIdle()
    assertTrue(viewModel.job.isCancelled)
}

2.4.3 使用ArchTaskExecutor和InstantTaskExecutorRule

适用于LiveData等同步执行场景。

class ViewModelLifecycleTest {
    @get:Rule
    val instantRule = InstantTaskExecutorRule()

    @Test
    fun testLiveDataEmitsOnCleared() {
        val viewModel = TestViewModel()
        viewModel.someLiveData.observeForever { /* 同步接收 */ }
        viewModel.onCleared()
        // 验证LiveData被置空或发出特定值
        assertEquals(null, viewModel.someLiveData.value)
    }
}
最佳实践:在测试中调用 onCleared() 后,应检查资源是否完全释放。对于使用 Closeable 的ViewModel,可继承 Closeable 接口,测试仅需验证 close() 被调用。

2.5 常见错误与调试

错误1:测试中未调用onCleared但断言资源释放
结果:测试通过,但生产环境因未释放引发内存泄漏。
解决方法:每个Test中显式调用 viewModel.onCleared(),或使用 @After 统一清理。
错误2:误认为onCleared在配置变更时也会触发
结果:在旋转测试中清除了不该清除的状态。
解决方法:使用 ViewModelProvider.get() 确保复用,不在onCleared中重置配置无关数据。
调试技巧:为ViewModel添加日志,在 onCleared 中输出当前状态。在单元测试中通过捕获日志(如使用JUnit的 TestRule)验证清除路径。

2.6 性能与稳定性优化

  • 避免在onCleared中执行耗时操作 – 如需保存数据,使用协程启动并设置超时。
  • 使用 viewModelScope 进行结构化并发 – 当onCleared触发时,自动取消所有子协程。
  • 利用 Closeable 接口 – 实现 AutoCloseable 的ViewModel,多个Closeable会自动依次关闭,简化测试。
  • 定期测试生命周期完整性 – 编写回归测试覆盖所有ViewModel的清除流程,防止后续修改破坏清理逻辑。