Skip to content

ViewModel 架构、SavedStateHandle 与进程被杀恢复 ​

回到总览:状态管理、路由、架构
相关模块:状态持久化、内存恢复与分层恢复策略 · Jetpack Compose 状态模型、StateFlow 与重组优化

一句话定义 ​

ViewModel 是 Jetpack 架构中负责管理 UI 相关数据并处理屏幕旋转等配置变更(Configuration Changes)的数据容器;结合 SavedStateHandle 机制,可以在 App 发生系统内存不足导致的进程被杀(Process Death) 时实现无感的状态恢复。

代码索引 ​

对应 Lab:viewmodel-savedstate-demo · ViewModelSavedStateDemoActivity.kt

为什么需要 ​

  • 为什么屏幕旋转时 Activity 会销毁重建,但 ViewModel 里的数据依然存在?
    • 一句话答:ViewModel 存储在宿主 Activity 的 ViewModelStore 中;在旋转重建时,系统通过 NonConfigurationInstances 机制保留了该 ViewModelStore 实例,新创建的 Activity 绑定同一个 Store 即可拿到旧 ViewModel。
  • 为什么有了 ViewModel 还需要 SavedStateHandle?
    • 一句话答:ViewModel 只能抵御“配置变更”(如旋转屏幕/切换语言);当 App 退后台由于系统内存不足被杀死(Process Death)时,ViewModel 依然会被清空,只有 SavedStateHandle(基于 onSaveInstanceState)能够跨进程杀死存活数据。
  • Compose 场景中 ViewModel 作用有何演进?
    • 一句话答:在 Jetpack Compose 中,viewModel() 函数将 ViewModel 绑定至 Navigation 路由节点或 Activity,作为页面级单一事实源(Single Source of Truth),隔离 Composables 纯渲染与 ViewModel 业务逻辑。

底层机制 ​

1. ViewModel 生命周期跨配置变更存活原理 ​

对应 Lab:viewmodel-savedstate-demo

图注补充:Act1: 原始 Activity (被销毁);Store: 首次创建 ViewModel 并存在 Store 中;Act1: onDestroy() (isChangingConfigurations = true);Store: 保留 ViewModelStore (不执行 store.clear());Store: 从 ViewModelProvider 重新获取;Store: onDestroy() (isFinishing = true)。

2. 进程被杀恢复 (SavedStateHandle) 的写入与还原 ​

图注补充:App 退后台 / 收到 onSaveInstanceState;SavedStateRegistry 序列化为 Bundle;系统内存不足杀掉 App 进程;从 SavedStateRegistry 恢复 Bundle;重新注入到新的 ViewModel.SavedStateHandle。

代码示例与 StateFlow 响应式结合 ​

kotlin
class UserSearchViewModel(
    private val savedStateHandle: SavedStateHandle,
    private val repository: UserRepository
) : ViewModel() {

    // 1. 将 query 状态绑定到 SavedStateHandle,支持进程被杀自动恢复
    var query: String
        get() = savedStateHandle.get<String>("KEY_QUERY") ?: ""
        set(value) { savedStateHandle["KEY_QUERY"] = value }

    // 2. 将 SavedStateHandle 转为 StateFlow
    val searchResult: StateFlow<List<User>> = savedStateHandle.getStateFlow("KEY_QUERY", "")
        .flatMapLatest { searchKey ->
            if (searchKey.isBlank()) flowOf(emptyList())
            else repository.searchUsersFlow(searchKey)
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )
}

Android / Flutter / Web / Backend 对照 ​

状态维度Android (ViewModel + SavedState)Flutter (Riverpod / BLoC)Web (Pinia / Vuex)
页面级状态ViewModel (生命周期感知)StateNotifier / CubitStore / Local Component State
进程杀死恢复SavedStateHandle / Bundlehydrated_bloc / PageStoragesessionStorage / IndexedDB
内存隔离ViewModel 不得持有 Context/View 强引用Notifier 不持有 BuildContextStore 不直接操作 DOM

常见场景 ​

1. 配合 Navigation 共享 ViewModel ​

在嵌套路由(Nested Navigation Graph)中,可以通过将 ViewModelProvider 作用域限定在 NavGraph 节点上,实现父子 Fragment/Composable 之间的数据无缝共享。

常见误配、事故后果与排障 ​

1. 事故:ViewModel 持有 Activity 或 View 的强引用导致内存泄漏 ​

  • 误配原因:在 ViewModel 内部声明了 var activity: Activity? 或 var view: View?。
  • 后果:当屏幕旋转时,旧 Activity 被销毁,但由于 ViewModel 生命周期比 Activity 长并强持有旧 Activity 引用,导致整个旧 Activity 树发生严重的内存泄漏!
  • 排障与修法:** ViewModel 严禁持有任何 Context 或 View 强引用**;若必须使用 Application Context,需继承自 AndroidViewModel。

2. 事故:SavedStateHandle 中存入超大 Bitmap 导致崩溃 ​

  • 误配原因:将用户头像的完整 Bitmap 直接存入 savedStateHandle["avatar"] = bitmap。
  • 后果:退后台触发 onSaveInstanceState 时,底层通过 Binder 传输序列化数据,触发 TransactionTooLargeException 崩溃!
  • 排障与修法:SavedStateHandle 仅用于存放轻量级主键(如 userId: String 或 id: Long),大数据必须持久化到 SQLite 或 Disk。

与相近概念对比 ​

保存机制存活范围能否抵御屏幕旋转能否抵御进程被杀推荐存储内容
ViewModel 内存进程存活期能否复杂 UI 状态、网络列表数据
SavedStateHandle进程死亡前 Bundle 存储能能页码、查询 Key、Form 选中 ID
Disk 持久化 (DataStore)长期保存能能用户 Token、设置项、草稿文本

对应实验 ​

Lab说明源码
viewmodel-savedstate-demoViewModel 跨旋转存活 + SavedStateHandle 进程恢复(Don't keep activities 验证)ViewModelSavedStateDemoActivity.kt · CounterViewModel.kt

复习检查题 ​

  1. 在 Android 中,为什么 ViewModel 严禁持有 Activity 或 View 的强引用?

    答:因为 ViewModel 的生命周期比 Activity 更加长久。当发生屏幕旋转等配置变更时,旧 Activity 实例会被销毁并重新创建,但 ViewModel 会保留在内存中。如果 ViewModel 持有了旧 Activity 或 View 的强引用,就会阻止 GC 回收旧 Activity 占用的整棵视图树,从而引发严重的内存泄漏。

  2. ViewModel 已经能抵御屏幕旋转了,为什么还需要 SavedStateHandle?

    答:因为 ViewModel 仅仅是将数据缓存在内存的 ViewModelStore 中。当应用退后台且系统内存不足时,系统会直接杀死 App 进程;此时内存中的 ViewModel 依然会被一并清空。而 SavedStateHandle 机制基于系统的 onSaveInstanceState,会将状态通过 Binder 暂存在系统 OS 进程中,使得进程被杀重新打开时依然能无感还原页面状态。

速记 ​

  • ViewModel 避坑:生命周期跨旋转,绝不持 Context/View 强引用。
  • SavedStateHandle:进程被杀能复原,轻量 ID 塞 Bundle,大图必须放 Disk。
  • 共享作用域:配合 Navigation Graph 实现父子页面级 ViewModel 无缝共享。

高频面试题还原 ​

以下为大厂面试中真实出现的问题场景还原,与上方「复习检查题」互补。

  1. 面试还原:「我们发现 App 被后台杀死后,用户重新打开列表页数据消失了,你怎么排查和修复?」

    答:数据消失说明 ViewModel 内存缓存被进程回收清空。排查链路:① 确认是系统杀进程(非用户主动关闭);② 查看是否启用了 SavedStateHandle 保存关键状态(如查询关键词、页码);③ 检查 onSaveInstanceState 写入的数据大小是否超限(Bundle 上限约 1MB)。修复:用 SavedStateHandle 保存轻量检索键,列表数据通过键从 Room/网络重新加载,而非把整个列表塞 Bundle。

  2. 面试还原:「ViewModel 的 viewModelScope 取消时机是什么?如果在协程里执行数据库写入,会不会因为 ViewModel 销毁而中断写操作?」

    答:viewModelScope 在 ViewModel.onCleared() 时取消,对应 Fragment/Activity 真正销毁(非旋转)。协程取消是协作式的,正在执行的挂起函数会在下一个挂起点抛出 CancellationException。数据库写操作若是单次挂起调用(如 Room suspend DAO),有可能在 cancel() 后被中断;关键写操作应改用 NonCancellable 上下文:withContext(NonCancellable) { db.insert(item) }。

  3. 面试还原:「同一个 Fragment 内不同的子模块需要共享一个 ViewModel,如何设计?」

    答:使用 Activity 作用域 ViewModel:viewModel<SharedViewModel>(ownerProducer = { requireActivity() });或使用 Navigation Graph 作用域:navGraphViewModel(R.id.my_graph)。推荐后者,范围更精确——只有同一导航子图内的 Fragment 共享,不污染整个 Activity 的生命周期,且用户导航离开该图后自动销毁。

  4. 面试还原:「我们的 ViewModel 里有一个 StateFlow,但 Fragment 切到后台后 collect 还在跑,造成了不必要的计算,怎么解决?」

    答:在 Fragment 中使用 repeatOnLifecycle(Lifecycle.State.STARTED) 代替普通 lifecycleScope.launch:

    kotlin
    viewLifecycleOwner.lifecycleScope.launch {
        viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
            viewModel.state.collect { /* 只在 STARTED 时运行,STOPPED 时自动暂停 */ }
        }
    }

    这样 Fragment 进入后台(STOPPED)时 collect 自动挂起,恢复前台时重新激活,避免后台无效计算和内存持有。

  5. 面试还原:「为什么 ViewModel 内不能直接用 LiveData.observe(viewLifecycleOwner),而推荐用 repeatOnLifecycle?」

    答:LiveData.observe 绑定 ViewLifecycleOwner,在 DESTROYED 时自动取消订阅,但不区分 STARTED 和 RESUMED,后台时仍可能分发数据触发 UI 更新(旧值补发)。repeatOnLifecycle(STARTED) 精确控制 collect 时机,且与 StateFlow/SharedFlow 等 Kotlin 协程生态无缝衔接,是 Google 推荐的现代写法。

站点构建时间:2026/8/24 23:43:17