4. ViewModel与状态管理:ViewModel的创建与作用域、ViewModelProvider与ViewModelStore原理、SavedStateHandle处理进程重建、跨Fragment共享ViewModel

各位同学,今天我们聊一个非常核心的话题——ViewModel。说实话,ViewModel 是 Jetpack 组件里我最早拥抱的一个。为什么?因为它在处理屏幕旋转、进程重建这些场景时,真的太省心了。

但很多同学只是会用,并不清楚它背后的原理。比如 ViewModel 到底存在哪里?为什么跨 Fragment 能共享同一个 ViewModel?进程被杀后数据怎么恢复?今天我们就把这些事彻底讲透。

核心要点:ViewModel 不是用来存大数据的,它的核心价值是帮你在配置变更(如屏幕旋转)时保住数据,以及在 Activity 重建时自动恢复。

4.1 ViewModel 的创建与作用域

先看一个最简单的用法:

class MyViewModel : ViewModel() {
    private val _count = MutableLiveData<Int>(0)
    val count: LiveData<Int> = _count

    fun increment() {
        _count.value = (_count.value ?: 0) + 1
    }
}

// 在 Activity 中使用
val viewModel = ViewModelProvider(this).get(MyViewModel::class.java)

这段代码大家应该很熟悉。但我想问一个问题:ViewModelProvider(this) 里的 this 是什么?

答案是 ViewModelStoreOwner。Activity 和 Fragment 都实现了这个接口。ViewModel 的作用域就由这个 owner 决定。

  • Activity 作用域:ViewModel 存活于 Activity 的整个生命周期,包括 onDestroy 后的重建过程。
  • Fragment 作用域:ViewModel 存活于 Fragment 的生命周期,Fragment 销毁后 ViewModel 也会被清理。
  • Activity 级别的 Fragment 共享:如果传入 activity 作为 owner,多个 Fragment 可以共享同一个 ViewModel。

我的经验:我个人习惯把 ViewModel 的作用域尽量缩小。能用 Fragment 级别的,就不用 Activity 级别的。因为作用域越大,内存占用时间越长,容易造成不必要的内存泄漏。

4.2 ViewModelProvider 与 ViewModelStore 原理

好,现在我们来扒一扒 ViewModelProvider 的底层。说白了,它就是一个工厂 + 缓存的管理器。

核心流程是这样的:

  1. ViewModelProvider 从 ViewModelStoreOwner 中拿到 ViewModelStore。
  2. ViewModelStore 内部维护了一个 HashMap<String, ViewModel>。
  3. 通过 key(默认是类名)去 HashMap 中查找,如果找到就直接返回。
  4. 如果没找到,就通过 Factory 创建新的 ViewModel,存入 HashMap,再返回。

看源码会更清楚:

// ViewModelProvider 的核心逻辑(简化版)
public class ViewModelProvider {
    private final ViewModelStore store;
    private final Factory factory;

    public <T extends ViewModel> T get(Class<T> modelClass) {
        String canonicalName = modelClass.getCanonicalName();
        // 先从缓存中取
        ViewModel viewModel = store.get(canonicalName);
        if (modelClass.isInstance(viewModel)) {
            return (T) viewModel;
        }
        // 缓存中没有,就创建
        viewModel = factory.create(modelClass);
        store.put(canonicalName, viewModel);
        return (T) viewModel;
    }
}

那 ViewModelStore 又存在哪里呢?答案是 Activity 的 NonConfigurationInstances 中。这个对象在 Activity 重建时会被保留下来。所以 ViewModel 才能躲过屏幕旋转的「劫难」。

注意:我曾经遇到过一个坑——在 Fragment 中通过 ViewModelProvider(requireActivity()) 获取 ViewModel,结果 Fragment 被销毁重建后,ViewModel 里的数据还在。这其实是符合预期的,因为作用域是 Activity 级别的。但如果你没搞清楚作用域,就会觉得「数据怎么没清掉?」

4.3 SavedStateHandle 处理进程重建

屏幕旋转这种配置变更,ViewModel 能轻松应对。但如果是进程被系统杀掉了呢?这时候 ViewModelStore 也会被销毁,数据就丢了。

怎么办?SavedStateHandle 就是来解决这个问题的。

它的原理很简单:ViewModel 通过 SavedStateHandle 来读写数据,这些数据会被自动保存到 Bundle 中,进程重建后从 Bundle 恢复。

class MyViewModel(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    // 直接从 SavedStateHandle 中读取,自动保存
    val count = savedStateHandle.getLiveData<Int>("count", 0)

    fun increment() {
        count.value = (count.value ?: 0) + 1
    }
}

// 创建时不需要手动传参,ViewModelProvider 会自动注入
val viewModel = ViewModelProvider(this).get(MyViewModel::class.java)

这里有个细节:ViewModel 的构造函数需要接收 SavedStateHandle,但 ViewModelProvider 会自动帮你注入。前提是你没有自定义 Factory,或者你的 Factory 继承了 AbstractSavedStateViewModelFactory。

避坑指南:我曾经在项目里自定义了 ViewModelProvider.Factory,结果忘了处理 SavedStateHandle,导致进程重建后数据全丢了。排查了半天才发现是 Factory 的问题。所以如果你要自定义 Factory,记得继承 AbstractSavedStateViewModelFactory。

SavedStateHandle 支持的数据类型和 Bundle 一样,比如 String、Int、Parcelable、Serializable 等。但要注意,它不适合存大数据,比如 Bitmap 或者大的 List。因为 Bundle 有 1MB 的大小限制。

4.4 跨 Fragment 共享 ViewModel

这个场景在实际开发中非常常见。比如一个主界面有列表 Fragment 和详情 Fragment,它们需要共享同一个数据源。

实现方式很简单:两个 Fragment 都使用同一个 Activity 作为 ViewModelStoreOwner。

// Fragment A
val sharedViewModel = ViewModelProvider(requireActivity()).get(SharedViewModel::class.java)

// Fragment B
val sharedViewModel = ViewModelProvider(requireActivity()).get(SharedViewModel::class.java)

这样 Fragment A 和 Fragment B 拿到的是同一个 ViewModel 实例。数据变化会实时同步。

但这里有一个容易踩的坑:Fragment 的生命周期问题

假设 Fragment A 先创建,它通过 requireActivity() 获取了 SharedViewModel。然后 Fragment B 被添加,也获取了同一个 ViewModel。一切正常。

但如果 Fragment A 被销毁了(比如从返回栈中移除),而 Fragment B 还在,这时候 SharedViewModel 依然存在,因为它的作用域是 Activity。但 Fragment A 中通过 LiveData 观察的数据,会因为 Fragment A 的销毁而自动解绑,不会造成内存泄漏。

我的建议:跨 Fragment 共享 ViewModel 时,最好在 ViewModel 中使用 LiveData 或 StateFlow 来暴露数据。这样 Fragment 在 onDestroy 时会自动取消订阅,避免内存泄漏。我见过有人直接用 MutableLiveData 的 setValue 来通知,结果 Fragment 销毁后还在更新 UI,导致空指针异常。

另外,如果你使用 Navigation 组件,还可以通过 navGraphViewModels 来限定 ViewModel 的作用域到某个导航图。这样只有该导航图内的 Fragment 才能共享这个 ViewModel。

// 在 Fragment 中获取导航图级别的 ViewModel
val viewModel: SharedViewModel by navGraphViewModels(R.id.nav_graph_id)

这种方式比 requireActivity() 更精确,不会污染整个 Activity 的 ViewModel 空间。

4.5 知识体系总览

为了让大家更直观地理解 ViewModel 的整个体系,我画了一张图:

ViewModel 知识体系总览 ViewModel 创建与作用域 ViewModelProvider 原理 SavedStateHandle 跨Fragment共享 Activity 作用域 Fragment 作用域 Navigation 图作用域 作用域越大,内存占用越久 ViewModelStore 缓存 HashMap<String, ViewModel> NonConfigurationInstances 配置变更时保留 进程重建恢复数据 Bundle 自动保存/恢复 getLiveData 自动管理 注意 1MB 大小限制 requireActivity() navGraphViewModels LiveData 自动解绑 避免内存泄漏 总结:ViewModel 的核心价值是「在生命周期变化时保住数据」 配置变更 → ViewModelStore 保留 | 进程重建 → SavedStateHandle 恢复 跨 Fragment 共享 → 同一 ViewModelStoreOwner

4.6 小结

好了,关于 ViewModel 的核心内容就讲到这里。我们回顾一下:

  • ViewModel 的创建依赖于 ViewModelStoreOwner,作用域由 owner 决定。
  • ViewModelProvider 通过 ViewModelStore 缓存 ViewModel,配置变更时数据不丢。
  • SavedStateHandle 解决进程重建问题,数据自动保存到 Bundle。
  • 跨 Fragment 共享 ViewModel 时,注意作用域和生命周期管理。

嗯,这些内容看起来不少,但说白了就是一句话:ViewModel 帮你管好数据,你只管写业务逻辑。下一章我们会聊 LiveData 和 StateFlow 的对比,到时候你会看到 ViewModel 和它们配合起来有多强大。


公众号:蓝海资料掘金营,微信deep3321