Jetpack 这个系列第一篇出来后不少做应用重构的朋友在后台问我ViewModel 已经用得挺熟了为什么还要去啃它的源码我的回答是如果你只是停留在by viewModels()的用法层面你是写不出既抗造又好维护的页面的。这次把 ViewModel 从创建到销毁整条链路讲透你就明白它为什么叫“组件”而不是“工具类”。做一个稍微复杂的电商详情页你会发现旋转屏幕后填写了一半的收货地址没了滑动到第三步的图片预览又跳回第一步网络请求还会莫名其妙重复发好几遍。ViewModel 就是官方针对这些场景给出的一套组件级方案但在项目里真正决定它能不能发挥价值的往往是你对它在配置变更、进程重建、生命周期回调这些关键节点上到底怎么运作的理解。这篇文章我会沿着一条主线走它为了解决什么问题而存在、内部三件套是怎么协作的、源码链路里旋转屏幕时到底发生了什么、以及你能直接抄进项目里的姿势和坑。1. 项目整体认知ViewModel到底解决什么问题1.1 没有ViewModel的日子手动保存状态的痛苦经历先说一个我早期的真实体验。当时做一个信息录入类的页面不复杂就是几个输入框加一个图片选择列表。测试时旋转了下屏幕结果用户刚填好的表单内容全没了选中的图片列表也回到了初始状态。那时候第一反应是赶紧把内容写到onSaveInstanceState里这个函数确实能把轻量数据放进 Bundle但等你把每个输入框、每个选中项、每个列表状态都手动塞进去代码已经变得非常恶心而且 Bundle 本身还有大小和类型限制存图片路径、缓存列表、分页信息都相当勉强。更麻烦的是请求问题。列表页旋转后 Activity 销毁重建onCreate又跑了一遍原有的网络请求也被触发一次。如果你没有做结果缓存用户看到的不是“刷新”而是“加载中”的闪烁严重的甚至会因为回调里的 View 已经失效而直接崩掉。当时也想过用单例来做临时数据中转但单例一旦 Activity 退出后没及时清理内存泄漏就成了定时炸弹。ViewModel 的价值恰恰就在这里它把页面里那些“只跟当前界面相关、不该随旋转丢失、又不值得持久化到数据库”的状态从 Activity 中抽离到一个独立对象里由官方机制保证它在配置变更期间存活。这相当于给你的页面准备了一个“出趟门回来东西还在原位”的抽屉而不是每次出门前匆匆忙忙把所有家当往兜里塞。1.2 配置变更与进程被杀的区别ViewModel能救哪个很多人有个认知误区觉得 ViewModel 是“持久化数据”的。这个必须纠正ViewModel 本质上是内存对象它解决的是配置变更configuration change带来的重建问题不是进程被杀后的数据恢复问题。配置变更指旋转屏幕、切换系统语言、深色模式切换这类事件。这些场景下 Activity 会被销毁重建但整个应用进程还活着堆内存还在系统也允许你在旧 Activity 销毁时把一些对象“托管”出去重建后再交回来。ViewModel 就是利用这个机制活下来的。进程被杀是另一回事。用户把应用切到后台系统内存紧张时直接回收掉整个进程清理掉所有堆内存Activity、ViewModel 全部烟消云散。当用户回到应用时系统会尝试恢复 Activity但你的 ViewModel 对象不会自动回来。要恢复这类场景你需要SavedStateHandle或onSaveInstanceState配合把轻量状态写进 Bundle由系统在进程重建时回传。所以正确的分工是ViewModel 负责解决旋转等配置变更时的状态保持SavedStateHandle 负责解决进程被杀后的临时状态恢复数据库和文件负责真正持久化。搞混这三层后面排查问题时会白费很多功夫。2. 原理拆解ViewModel是怎么“活”过配置变更的2.1 核心三件套ViewModelStoreOwner、ViewModelStore、ViewModelProviderViewModel 能在配置变更后存活靠的是三个核心接口和类之间的协作ViewModelStoreOwner、ViewModelStore、ViewModelProvider。ViewModelStoreOwner是一个接口定义了getViewModelStore()方法。Activity 和 Fragment 都实现了它意味着它们是 ViewModel 的“宿主容器”。没有这个宿主ViewModel 就没有存身之所。ViewModelStore是真正存放 ViewModel 的容器内部结构就是一个HashMapString, ViewModel按 key 存、按 key 取。它有几个关键方法get(key)、put(key, viewModel)、clear()。整个 store 的生命周期由一个宿主负责维护。ViewModelProvider是使用入口负责帮你从 store 里拿到目标 ViewModel。它的逻辑很直白先从 store 里通过 key 找找到且类型匹配就直接返回找不到就用内部的 Factory 创建新实例再放进 store。Factory 则是创建 ViewModel 的工厂接口只有一个方法create(ClassT modelClass): T。默认实现DefaultViewModelProviderFactory会尝试通过无参构造newInstance()创建实例。如果你需要给 ViewModel 传参就要自己提供 Factory。这里值得注意的设计意图是Provider 依赖的是“这个 Activity 对应哪个 ViewModelStore”而不是“Activity 的实例是谁”。所以哪怕 Activity 换了个新实例只要它从系统手里拿回了同一个 ViewModelStore就能从里面取出同一个 ViewModel。这就像一个储物柜锁和钥匙换了但柜子还是原来那个柜子里面的东西自然还在。2.2 源码链路走查旋转屏幕时究竟发生了什么把上面这些类串起来走一遍旋转屏幕的完整链路。第一步屏幕旋转触发配置变更系统决定销毁旧的 Activity创建新的 Activity。旧 Activity 会在销毁流程中走到onRetainNonConfigurationInstance()这个方法。ComponentActivity已经重写了它在处理时会构造一个内部对象NonConfigurationInstances把当前 Activity 持有的ViewModelStore实例装进去交给系统临时保留。这个对象不会随 Activity 销毁而消失而是由系统放在“非配置实例”通道里等着新 Activity 来取。第二步新 Activity 创建时会在onCreate前调用的getLastNonConfigurationInstance()方法中取回之前保存的NonConfigurationInstances再从里面恢复出那个一模一样、还是原来内存地址的ViewModelStore实例重新赋给新 Activity。第三步代码里调用ViewModelProvider.get(ViewModel::class.java)时Provider 从新 Activity 拿到的 store 里查询目标 key发现旧实例还在于是直接返回。你看到的现象就是“Activity 变了ViewModel 没变”。看一段简化版的ViewModelProvider.get()内部逻辑就能感受到这个流程的简单和优雅public T extends ViewModel T get(NonNull String key, NonNull ClassT modelClass) { // 1. 先从 store 里查命中直接返回 ViewModel viewModel mViewModelStore.get(key); if (modelClass.isInstance(viewModel)) { return (T) viewModel; } // 2. 没找到就通过 Factory 创建放回 store viewModel mFactory.create(modelClass); mViewModelStore.put(key, viewModel); return (T) viewModel; }整个过程最核心的一句话是旋转时被保留下来的不是 Activity 实例而是 Activity 内部那棵 ViewModel 存储树。旧 Activity 销毁前把树托管给系统新 Activity 创建后把同一棵树接回来。这个机制在 Fragment 的场景里同样成立只是托管链路的载体换成 FragmentManager 的NonConfig机制。2.3 onCleared()的精确触发时机与边界条件ViewModel 的回调方法只有一个onCleared()它是资源释放和任务取消的唯一时机。很多人都知道它会在页面退出时调用但具体什么时候不调用往往说不清。关键就在 ActivityonDestroy()里的这段逻辑override fun onDestroy() { // 判断当前销毁是不是因为配置变更 if (!isChangingConfigurations) { viewModelStore.clear() } }当用户按返回键退出、或者调用finish()时isChangingConfigurations为 falseActivity 销毁后才会调用viewModelStore.clear()进而触发每个 ViewModel 的onCleared()。当只是旋转屏幕或切换暗黑模式时isChangingConfigurations为 true即使 Activity 的onDestroy被执行了store 也不会被 clearViewModel 也就不会收到onCleared()。这个判断从根上保证了 ViewModel 在配置变更期间不会“假死”或提前释放。Fragment 场景还有一个容易踩的点Fragment 被替换时如果还在回退栈里它的 ViewModelStore 不会 clear只有 Fragment 真正被移除、且不再存在于回退栈时才会触发清理。所以不要指望 Fragment 从回退栈弹出来时onCleared()一定马上执行。这属于正常机制不是 Bug。3. 生命周期感知的完整闭环ViewModel如何与Lifecycle协同3.1 ViewModel决不会给你View引用这是一条铁律ViewModel 名字里带“View”但它跟 View 没有任何直接关联这是一个非常容易误用的点。ViewModel 的存续期比 Activity 和 Fragment 长如果在 ViewModel 里持有 Activity、View 或 LifecycleOwner 的引用页面退出但 ViewModel 还没清理时这些引用会阻止 GC 回收整个 UI 树造成内存泄漏。LeakCanary 上报的很大一类问题就是这么来的。从设计意图上看ViewModel 是“面向 UI 状态”的不是“面向 UI 控件”的。它只存储和加工数据列表数据、加载状态、用户输入、临时缓存。UI 层应该观察 ViewModel 暴露出的数据然后自己去渲染。LiveData、StateFlow、Flow 这些响应式组件就是为了这个“数据驱动 UI”的链路存在的。有个直观的类比ViewModel 是门店后厨Activity 是前台服务员。后厨只管把菜做好放到出餐口前台自己去取、去端给顾客。如果后厨派人跑到前厅站着等服务员点完菜再炒整个门店就乱套了生命周期稍长一点这个人还会赖在店里不走。3.2 对比一下ViewModel、SavedInstanceState、持久化的分工搞清楚 ViewModel 的位置最好的办法是把它和另外两层存储机制放在一张表里对比。维度ViewModelSavedStateHandle / Bundle数据库 / 文件本质载体内存对象Bundle 序列化数据持久化存储旋转屏幕时存活继续使用系统自动保留不受影响进程被回收时丢失可恢复轻量数据不受影响容量限制内存足够即行有大小限制不适合大对象无上限典型用途页面数据、列表缓存、UI 状态表单临时草稿、筛选条件、滚动位置用户信息、业务数据这个表格写出来你会发现页面状态的解决方案从来不是“二选一”而是三层叠加。ViewModel 管内存态SavedStateHandle 管进程重建恢复数据库管持久业务数据。每一层的工作边界都别越界代码的稳定性就会好很多。我曾经见过一个项目执着于把所有状态都塞进 ViewModel结果进程被杀后用户返回页面一片空白那就是没有第二层兜底。4. 实操经验ViewModel的正确使用姿势与进阶模式4.1 构造参数与工厂模式什么时候写ViewModelProvider.Factory想用好 ViewModel第一个绕不开的问题就是构造参数。默认情况下ViewModelProvider 通过无参构造函数创建 ViewModel所以下面的代码能直接跑通class SimpleViewModel : ViewModel() { val title MutableStateFlow() }可一旦你的 ViewModel 需要依赖 Repository、数据源或者某个业务参数比如“详情页的 ArticleId”你就会遇到经典的Cannot create an instance of class ViewModel报错。解决办法就是自定义 Factory。现在用 Kotlin 写已经不需要手写一长串模板了class ArticleViewModel( private val articleId: Long, private val repo: ArticleRepository ) : ViewModel() { companion object { fun factory(articleId: Long): ViewModelProvider.Factory { return viewModelFactory { initializer { ArticleViewModel(articleId, ArticleRepository()) } } } } }Activity 里搭配by viewModels委托使用private val articleVM: ArticleViewModel by viewModels { ArticleViewModel.factory(articleId intent.getLongExtra(id, 0L)) }注意viewModelFactory和initializer这两个 API 来自lifecycle-viewmodel-ktx需要单独引入依赖。使用 Factory 的模式比把参数塞到SavedStateHandle或者全局变量里都要清晰。构造器里直接声明依赖测试时就能传入假的 Repository这也是推荐依赖注入的起点。如果你还在用AndroidViewModel拿 Application那属于历史遗留方案能用但不够优雅建议尽早迁到构造器注入。4.2 跨Fragment共享数据的官方正确思路Activity 内多个 Fragment 想共享数据常见做法有 EventBus、共享单例其实官方早就给出了更安全的方式使用同一宿主 Activity 的 ViewModelStore。在 Fragment 里不要用by viewModels()它会把 ViewModel 存到 Fragment 自己的 store要用by activityViewModels()class SharedViewModel : ViewModel() { private val _refreshState MutableStateFlowRefreshState(RefreshState.Idle) val refreshState: StateFlowRefreshState _refreshState.asStateFlow() fun onRefresh() { ... } }// FragmentA 和 FragmentB 都写这一行 private val sharedVM: SharedViewModel by activityViewModels()这里面的原理不神秘Fragment 调用activityViewModels()时使用的是宿主 Activity 的ViewModelStore所以几个 Fragment 取到的都是同一个实例。相比 EventBus它没有“发送了没人收”的问题所有数据都天然跟着宿主 Activity 走宿主退出时自动清理不会出现全局单例残留。在实际项目里要特别注意一点不要直接把MutableStateFlow或MutableLiveData整个暴露给其他 Fragment。更好的做法是用私有可变版本加公有只读版本再配合状态类做单向数据流。否则业务复杂以后你会发现自己都不知道谁改了数据。4.3 与协程、Flow、LiveData组合的推荐写法ViewModel 和协程的搭配官方已经默认集成好了。只要引入lifecycle-viewmodel-ktxViewModel 内部就会多一个viewModelScope()它默认跑在Dispatchers.Main.immediate生命周期跟随 ViewModelonCleared()时自动取消所有协程。这意味着你在 ViewModel 里发起的网络请求用户退出页面时不会被遗留到后台继续空转也不会因为回调里还拿着 View导致崩溃或泄漏。看一个比较推荐的写法class HomeViewModel : ViewModel() { private val repo HomeRepository() val uiState: StateFlowHomeUiState repo.fetchHomeData() .map { HomeUiState.Success(it) } .catch { emit(HomeUiState.Error(it)) } .stateIn( scope viewModelScope, started SharingStarted.WhileSubscribed(5000), initialValue HomeUiState.Loading ) }关于 LiveData 和 Flow我的建议是状态类数据优先用StateFlow一次性事件则用SharedFlow或回调接口。StateFlow天然携带最新值访问value就能拿到当前状态还支持丰富的操作符跟协程衔接顺畅。不过要提醒一句在 UI 层用repeatOnLifecycle包住 collect防止页面切到后台后依然持续接收数据流。这块不小心很容易成为性能上“看不见的敌人”。5. 常见问题与排查记录我踩过的坑下面整理一份我在实际项目中踩过、也帮别人排查过的问题清单每一条都不是冷僻场景而是很容易在日常开发中反复出现。问题现象原因与处理ViewModel 持有 View 引用LeakCanary 报告页面销毁后仍有泄漏ViewModel 生命周期比 View 长不能在 ViewModel 里保存 Activity / View / Fragment 引用改用数据回调驱动 UI页面旋转后再次发起网络请求明显多发了一倍请求页面闪烁一次请求写在了onCreate里且 ViewModel 数据是空的应该在 ViewModel init 中发请求并通过状态缓存判断是否已有数据Fragment 共享数据时取到不同实例两个 Fragment 状态不同步一个用了viewModels()另一个用了activityViewModels()两者作用域不同必须全部指向同一个宿主建议全局统一进程被回收后页面状态丢失用户切后台一段时间再回来页面数据空白只有 ViewModel 没有配SavedStateHandle临时状态需要走进程重建恢复通道无限刷新或重复加载数据流被多个观察者重复订阅stateIn策略没配好或 UI 层没在repeatOnLifecycle中收集统一数据入口并限制订阅生命周期在 Fragment 中依赖 activityViewModels 但页面销毁不调 onCleared关闭页面后数据仍占用资源若宿主 Activity 仍在存活Fragment 显式移除后才触发清理不要把页面级的资源放在共享 ViewModel 里无限累积排查时的思路也有规律先确认“ViewModel 实例是不是同一个”再确认“是哪个 ViewModelStore 在管”最后才去怀疑“是配置变更的问题还是进程被回收的问题”。三个问题定位清楚这类错误基本上十有八九能找到根因。另一个容易忽视的是SavedStateHandle的添加时机。很多人只会在遇到进程恢复问题时才想起来加但临时状态保存这件事最好在 ViewModel 设计的初始阶段就放进方案里。比如一个订单筛选页筛选条件、搜索关键词、滚动位置都建议通过SavedStateHandle保存。这样用户切后台再回来体验是完整的不用额外请求一次数据。6. 扩展认识从原理出发看懂ViewModel的开发演进6.1 SavedStateHandle让配置变更与进程回收都不怕前文反复提到SavedStateHandle这里展开细讲一下。它是 ViewModel 构造器中的一个特殊参数由系统在创建 ViewModel 时自动提供。日常写法极其简洁class CountViewModel( private val savedStateHandle: SavedStateHandle ) : ViewModel() { val count savedStateHandle.getStateFlow(count, 0) fun increase() { savedStateHandle[count] (savedStateHandle[count] as? Int ?: 0) 1 } }内部机制上SavedStateHandle包装了一个 Bundle。当 Activity 因配置变更或进程回收重建时系统会从 Bundle 中恢复数据并把对应的SavedStateHandle传给新 ViewModel。它比onSaveInstanceState的手写版本更友好还支持getStateFlow、getLiveData这类响应式 API。要注意SavedStateHandle不适合存大对象和二进制数据。它最终走的是 Bundle 通道超过一定体积会触发TransactionTooLargeException。列表数据还是放 ViewModel 内存里SavedStateHandle 只处理轻量状态。6.2 从Factory演进看DI框架的介入理解 Factory 原理之后你会更容易理解 Hilt 这类依赖注入框架在 ViewModel 上做了什么。Hilt 通过HiltViewModel注解自动生成 ViewModel 的 Factory并把SavedStateHandle和业务依赖一起注入到构造器里HiltViewModel class DashboardViewModel Inject constructor( private val repo: DashboardRepository, private val savedStateHandle: SavedStateHandle ) : ViewModel()这样项目里就不再需要手写companion object { factory() }依赖关系一目了然测试时也方便替换 mock。但如果你没搞懂默认 Factory 和无参构造的关系遇到 Hilt 集成报错时依然会一头雾水。就见过的案例来说很多人看到Cannot create an instance of class ViewModel就只知道上网搜实际只要沿着 Factory 这条线马上就能定位到“哪个依赖没注入”和“哪个构造器没有被正确理解”。原理在手解决问题的速度会快很多。我个人在实际项目里的体会是ViewModel 真正厉害的地方不是某个 API而是它逼着你把“界面”和“状态”拆开。早期写页面时Activity 里塞满了数据请求、刷新逻辑、状态判断后期加一个需求要动好几处。用 ViewModel 之后我习惯先把页面会出现的所有状态写成一个不可变状态模型再围绕它组织数据流和交互逻辑最后才去写 UI。这个思考顺序一旦建立重构页面就像重新排列组合积木而不是推倒重砌墙。如果你打算把现有项目迁移到更清晰的架构上建议从最常动的那个页面开始试把状态抽进 ViewModel再逐步推广效果会比想象中明显。