深入理解Android ViewModel和SavedStateHandle

48 阅读10分钟

一、ViewModel

ViewModel 的核心原理是通过一个独立于UI(Activity/Fragment)生命周期的容器,来存储和管理界面所需的数据。其设计的精髓在于数据与UI生命周期的分离,由 ViewModelStore 负责在屏幕旋转等配置变更时保留数据。

下面这张流程图清晰地展示了 ViewModel 的创建、存储和生命周期管理的核心机制:

flowchart TD
    A[Activity/Fragment 首次创建] --> B[初始化 ViewModelStore]
    B --> C[通过 ViewModelProvider.get 请求 ViewModel]
    
    C --> D{"ViewModelStore 中<br>是否存在实例?"}
    D -- 是 --> E[返回已存在的实例]
    D -- 否 --> F[通过 Factory 创建新实例]
    F --> G[存入 ViewModelStore 并返回]
    
    E --> H[屏幕旋转等配置变更]
    G --> H
    
    subgraph Z[配置变更期间]
        H --> I["Activity 被销毁,但<br>ViewModelStore 被系统保留"]
        I --> J[新的 Activity 实例被创建<br>并接收到保留的 ViewModelStore]
    end
    
    J --> C
    
    K["Activity 真正销毁<br>(如返回或退出)"] --> L[ViewModelStore.clear<br>触发所有 ViewModel.onCleared]

1. 核心原理深度解析

理解上图中的几个核心组件是关键:

  • ViewModelStore: 如图中所示,它是一个简单的键值对容器(内部是 HashMap),是 ViewModel 实例的“储藏室”。它的特殊之处在于,当 Activity 因配置变更被销毁重建时,它会被系统通过 onRetainNonConfigurationInstance() 机制临时保留下来,并传递给新的 Activity 实例,从而实现 ViewModel 的“存活”
  • ViewModelProvider: 这是获取 ViewModel 的统一入口。它遵循  “get-or-create”  模式:当使用 ViewModelProvider(owner).get(MyViewModel::class.java) 时,它会先以类的规范名称为 Key,去属于 owner(如 Activity)的 ViewModelStore 中查找;如果找到就直接返回,如果没找到则通过 Factory 创建并存入 ViewModelStore
  • viewModelScope: 这是 ViewModel 的一个 Kotlin 扩展属性,它绑定了一个与 ViewModel 生命周期一致的协程作用域。当 ViewModel 被清除时(onCleared()),此作用域内的所有协程会自动取消,有效避免了内存泄漏和后台任务积压

2. ViewModel 在架构中的角色与协作

ViewModel 是 MVVM(Model-View-ViewModel)  架构模式的核心组件,负责连接 View(UI)  和 Model(数据层/业务逻辑)

  • 职责划分:

    • View (UI控制器) : 仅负责显示数据和接收用户输入,不处理业务逻辑。
    • ViewModel: 负责为 View 准备和暴露 UI 所需的数据状态(State),并处理来自 View 的用户交互事件,向 Model 层发起请求。
    • Model: 负责数据获取和持久化(如数据库、网络)。
  • 数据流: 通常遵循单向数据流原则

    1. 状态下行:Model 提供数据 -> ViewModel 加工为 UI 状态 -> View 观察并渲染。
    2. 事件上行:View 触发用户事件 -> ViewModel 处理 -> 必要时通知 Model 更新数据。
  • 与 LiveData/StateFlow 的协作
    ViewModel 内部通常使用 MutableLiveData 或 MutableStateFlow 来持有可变的 UI 状态,但对外仅暴露其只读版本(LiveData 或 StateFlow)。这样既保证了数据可被观察,又封装了修改权,确保数据变化只能通过 ViewModel 的特定方法进行,符合单向数据流。

3. 要点详解

3.1 基础与原理类

  • ViewModel 是什么?解决了什么问题?

    ViewModel 是一个生命周期感知的组件,用于以结构化的方式存储和管理与 UI 相关的数据。它核心解决了屏幕旋转等配置变更导致的数据丢失问题,同时帮助实现了 UI 控制器(Activity/Fragment)与业务逻辑的关注点分离

  • ViewModel 如何在屏幕旋转后存活?

    其核心机制是 ViewModelStore。配置变更时,Activity 被销毁,但 ViewModelStore 会被系统通过 onRetainNonConfigurationInstance() 机制临时保留下来。新的 Activity 实例创建后,会接收到同一个 ViewModelStore,从而获取到里面保存的 ViewModel 实例

  • ViewModel 和 onSaveInstanceState() 的区别?

    这是经典面试题。主要区别在于数据大小、存储位置和用途

    • onSaveInstanceState() : 用于保存少量、可序列化的 UI 状态(如文本框内容),以应对系统内存不足、进程被杀后恢复的场景。数据会序列化到磁盘。
    • ViewModel: 用于存储和管理相对较大、不可序列化的 UI 数据(如用户列表、网络请求结果),仅在同一进程内的配置变更(如旋转)时存活。数据保存在内存中。

3.2 使用与进阶类

  • 如何正确创建 ViewModel?为何不能直接 new

    必须通过 ViewModelProvider 来获取实例。如果直接 new,创建的 ViewModel 无法与当前生命周期的所有者(如 Activity)关联,既无法在配置变更时保留,其 viewModelScope 也无法正确销毁,会导致内存泄漏

  • ViewModel 可以持有 Context 吗?

    避免持有 Activity 等生命周期短的 Context 引用,以防内存泄漏。如果确实需要应用上下文,可以使用 AndroidViewModel(它是 ViewModel 的子类,内部持有 Application Context)。

  • 如何在 Fragment 间共享数据?

    让 Fragments 共享它们宿主 Activity 作用域的 ViewModel。使用 by activityViewModels() 委托来获取同一个实例,从而实现数据共享和通信

  • StateFlow 与 LiveData 如何选择?

    特性LiveDataStateFlow
    生命周期感知原生支持,自动管理需要配合 repeatOnLifecycle 等手动控制
    数据重放默认仅最新值始终重放最新值给新订阅者
    协程支持有限原生支持,基于协程
    适用场景简单 UI 状态、Java 项目复杂数据流、纯 Kotlin 项目、需要更多操作符

    在新项目中,如果全面使用 Kotlin 协程,更推荐使用 StateFlow,因为它提供更严格的线程控制和丰富的变换操作。如果项目简单或仍需支持 Java,LiveData 仍是好选择。

  • 如何防止 viewModelScope 造成的内存泄漏?

    viewModelScope 的设计已经考虑了生命周期,它会在 ViewModel 的 onCleared() 时自动取消。开发者需要注意:

    1. 不要在 viewModelScope 中启动无限循环的协程。
    2. 在协程内部进行耗时操作时,应使用 try/catch 或 CoroutineExceptionHandler 妥善处理异常,避免因未捕获异常导致作用域取消链断裂。
    3. 对于需要超出 ViewModel 生命周期的任务(如上传日志),应使用 Application 作用域的协程或其他机制。

二、SavedStateHandle

当应用进程被系统杀死(Process Death)后,SavedStateHandle 是确保关键 UI 状态能够恢复的关键组件。它与 ViewModel 集成,提供了一种比 onSaveInstanceState 更优雅、更内聚的数据保存方案。

1. 进程死亡 vs. 配置变更

首先必须明确这两者的根本区别,这也是理解 SavedStateHandle 必要性的前提:

场景触发条件数据存储位置ViewModel 是否存活恢复机制
配置变更屏幕旋转、语言切换、深色模式切换等。内存 中(通过ViewModelStore保留)。,ViewModel实例被保留。自动,系统重建Activity并重新关联同一个ViewModel。
进程死亡系统资源不足、用户长时间切到后台、应用发生崩溃。磁盘 上(由系统将Bundle写入磁盘)。,整个应用进程被销毁,内存清空。手动,系统重建Activity和ViewModel,开发者需从SavedStateHandle恢复数据。

简单来说,SavedStateHandle 就是 ViewModel 为应对进程死亡场景而配备的“逃生背包”。它允许你将必要的状态(如当前列表的滚动位置、临时填写的表单内容)打包存盘,在用户回来时恢复现场。

2. SavedStateHandle 核心原理与工作流程

其核心原理是:将数据保存到系统管理的 Bundle 中,该 Bundle 最终会通过 Activity 的 onSaveInstanceState() 机制写入磁盘,并在重建时恢复

它的工作流程可以通过下图完整展示:

flowchart TD
    A["用户在界面操作<br>(如编辑、滚动)"] --> B["ViewModel 通过 SavedStateHandle<br>更新状态 (如 set)"]
    B --> C[SusavedStateHandle 将数据存入<br>内部 Bundle]
    
    subgraph D [进程死亡与恢复周期]
        D1[系统因内存不足<br>杀死应用进程] --> D2[触发 Activity.onSaveInstanceState<br>SavedStateRegistry 将 Bundle 写入磁盘]
        D2 --> D3["用户返回,系统重建进程"]
        D3 --> D4[创建新 ViewModel 实例<br>并注入恢复的 Bundle]
    end
    
    C -.->|"“自动关联”"| D2
    D4 --> E[新 ViewModel 的 SavedStateHandle<br>包含恢复的数据]
    E --> F[UI 通过 LiveData/StateFlow<br>观察到恢复的状态并刷新]

关键实现细节

  • 自动序列化SavedStateHandle 内部使用 BaseSavedStateRegistry,能自动处理可序列化(Serializable/Parcelable)的数据类型。对于复杂对象,需要手动转换(如转为 JSON 字符串)。
  • 与 ViewModel 集成:当通过 ViewModelProvider 创建带有 SavedStateHandle 参数的 ViewModel 时,系统会自动将恢复的 Bundle 注入。
  • 自定义保存逻辑:通过 SavedStateRegistry 的 registerSavedStateProvider 方法,可以在保存时刻动态生成要保存的状态,适用于那些无法实时同步到 SavedStateHandle 的复杂数据。

3. 与传统 onSaveInstanceState 的对比

SavedStateHandle 在架构上更加先进:

方面传统 onSaveInstanceStateViewModel + SavedStateHandle
保存位置Activity/Fragment 中。ViewModel 中,与数据逻辑放在一起。
职责分离违反单一职责,UI控制器需要关心数据保存。符合关注点分离,ViewModel负责数据持久化。
数据访问需要在onCreate中读取Bundle并手动分发。可观察,通过getLiveData()直接暴露给UI观察。
类型安全从Bundle存取数据需要键名,容易出错。通过键值对存取,并与LiveData/StateFlow结合,类型更安全。
测试便利性难以模拟系统保存和恢复的Bundle。ViewModel可以独立测试,SavedStateHandle可轻松模拟。

4. 在 ViewModel 中使用 SavedStateHandle

在 ViewModel 的构造函数中添加 SavedStateHandle 参数即可使用:

// 1. 在 ViewModel 中接收 SavedStateHandle
class MyViewModel(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    // 2. 定义状态键(推荐使用常量)
    companion object {
        private const val SEARCH_QUERY_KEY = "search_query"
        private const val SELECTED_ITEM_ID_KEY = "selected_item_id"
    }

    // 3. 方式一:获取一个与状态绑定的 LiveData
    // 初始值来自 savedStateHandle(如果进程死亡后恢复,则有值;否则为默认值)
    val searchQuery: LiveData<String> = 
        savedStateHandle.getLiveData(SEARCH_QUERY_KEY, "")

    fun updateSearchQuery(query: String) {
        // 更新 LiveData 的值,同时会自动保存到 SavedStateHandle
        savedStateHandle[SEARCH_QUERY_KEY] = query
    }

    // 4. 方式二:获取一个与状态绑定的 StateFlow (推荐用于 Kotlin 项目)
    val selectedItemId: StateFlow<Int?> = 
        savedStateHandle.getStateFlow(SELECTED_ITEM_ID_KEY, null)

    fun selectItem(id: Int) {
        savedStateHandle[SELECTED_ITEM_ID_KEY] = id
    }

    // 5. 方式三:直接存取(适用于不需要观察的简单数据)
    fun saveTempData(value: String) {
        savedStateHandle["temp_key"] = value
    }
    fun getTempData(): String? = savedStateHandle.get<String>("temp_key")

    // 6. 移除不需要持久化的数据
    fun clearTempData() {
        savedStateHandle.remove("temp_key")
    }
}

在 Activity/Fragment 中,你无需做任何特殊处理,像普通 ViewModel 一样获取即可:

// Activity/Fragment 中,像往常一样获取 ViewModel
// 系统会自动处理 SavedStateHandle 的注入
private val viewModel: MyViewModel by viewModels()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    // 直接观察来自 SavedStateHandle 的 LiveData/StateFlow
    viewModel.searchQuery.observe(viewLifecycleOwner) { query ->
        // UI 会自动接收到恢复的搜索词
        editText.setText(query)
    }
}

5. 最佳实践与要点详解

5.1 使用原则

  • 存小存精:只保存最小必要的 UI 状态(如 ID、关键字、索引),而非完整数据对象(如整个用户列表)。完整数据应从 Model 层(数据库、网络)重新加载。
  • 数据类型:优先保存 ParcelableSerializable, 基础类型(IntString)。复杂对象需手动序列化(如使用 Gson 转为 JSON 字符串)。
  • 组合使用:将 SavedStateHandle 用于“进程死亡恢复”,将 ViewModel 的常规属性用于“配置变更保留”,将 Room 数据库用于“永久存储”。

5.2 要点详解

  • SavedStateHandle 与 onSaveInstanceState 有什么区别和联系?

    联系:底层都通过 Activity 的 onSaveInstanceState 机制将 Bundle 写入磁盘。
    区别SavedStateHandle 将保存逻辑上移到了 ViewModel,使数据保存成为业务逻辑的一部分,而不是视图控制器的职责,更符合架构原则。

  • ViewModel 已经能在旋转后保存数据,为什么还需要 SavedStateHandle

    这是为了应对  “进程死亡”  这一更彻底的数据销毁场景。ViewModel 的存活依赖内存,而进程死亡后内存清空。SavedStateHandle 通过磁盘备份,提供了最后一道保障。

  • 什么数据应该用 SavedStateHandle 保存?

    应保存临时、易失、但直接影响用户体验的状态。例如:

    • 用户正在填写的表单内容(未提交)。
    • 列表的滚动位置。
    • 当前选中的标签页或筛选条件。
    • 导航栈中的当前目的地(如结合 Navigation Component)。
  • 如何测试包含 SavedStateHandle 的 ViewModel?

    可以使用 SavedStateHandle.createHandle() 创建测试用的句柄:

    @Test
    fun testSavedStateHandle() {
        // 1. 创建模拟的 SavedStateHandle 并存入初始状态
        val savedStateHandle = SavedStateHandle.createHandle(
            null, // 没有恢复的 Bundle
            null
        )
        savedStateHandle["key"] = "initial_value"
    
        // 2. 传入 ViewModel 进行测试
        val viewModel = MyViewModel(savedStateHandle)
    
        // 3. 断言行为
        assertEquals("initial_value", viewModel.getSomeData())
    }
    

总结来说,SavedStateHandle 是构建健壮 Android 应用的重要工具,它巧妙地将进程死亡恢复这一系统级机制,封装成了 ViewModel 内部一个易于管理和测试的组件。正确使用它,能让你的应用在极端情况下依然为用户提供连贯的体验。