尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android ViewModel源码解析:从生命周期管理到SavedStateHandle实践

Android ViewModel源码解析:从生命周期管理到SavedStateHandle实践 1. 从一场“配置变更”说起没有了 ViewModelAndroid 的界面数据到底有多脆弱1.1 旋转屏幕后的第一反应数据哪儿去了先还原一个所有 Android 开发者都经历过的场景你在页面上填了一个表单填到第三项手指刚划过屏幕不小心碰到了旋转感应屏幕一翻转Activity 被重建。等你再抬起头刚才填的内容全部消失。早期没有 ViewModel 的时候这个问题让很多人头疼。处理方式无非三种用onSaveInstanceState把数据序列化到 Bundle 里重建后再恢复把数据放到静态变量或者干脆在manifest.xml里给 Activity 加上android:configChangesorientation。三种方案各有各的别扭第一种对复杂对象非常痛苦第二种一杀进程就全没还会造成内存泄漏第三种属于静态逃避还会带来 Canvas 尺寸适配问题。ViewModel 之所以被 Google 放到 Android 架构组件的第一梯队核心目标就是解决这个根上一直没断过的痛点——配置变更导致的界面数据丢失。它把数据和 UI 的生命周期解耦让数据实例跟随 Activity 或 Fragment 的“范围”存活而不是跟随 View 的存活周期。听起来很抽象但从源码看下去你会发现这套设计比很多人理解的“一个简单的缓存类”要细致得多。1.2 ViewModel 的官方定义被很多人低估了官方文档对 ViewModel 的描述只有两句话以生命周期意识的方式存储和管理界面相关的数据允许数据在配置变更如屏幕旋转后继续留存。但如果你只是把这个类当作“Activity 旋转时的数据保险箱”那么你对 ViewModel 的利用可能只有它能力的百分之二十。源码里ViewModel 被设计成一个轻量级的基类核心成员非常克制——内部维护一个mBagOfTags用于存放一些附加对象用closeWithRuntimeException处理清理逻辑对外暴露的只有一个clear()方法和一个onCleared()钩子。// 来自 androidx.lifecycle.ViewModel 原始源码 MainThread final void clear() { mCleared true; if (mBagOfTags ! null) { synchronized (mBagOfTags) { for (Object value : mBagOfTags.values()) { if (value instanceof Closeable) { ((Closeable) value).close(); } } } } onCleared(); }看得出onCleared()才是 ViewModel 的“临终告别”方法真正干活的clear()并不是 public 的而是一个final方法只能由 Android 框架内部调用。这个细节说明了一个关键设计开发者不能手动调用onCleared()生命周期结束时机的判断权完全交给框架。这也是 ViewModel 你敢放任何数据进来的底气——它知道自己在合适的时刻被清理而且不会依赖开发者自觉。所以看待 ViewModel 的正确姿势是它是一个由生命周期框架管理生命周期的、可保存跨配置变更数据的数据容器。这个定义很朴素但它是一切上层变化的基础。2. 拿到一个 ViewModel 的完整来源链路从 get() 到反射创建再回到缓存2.1 ViewModelProvider 的三个关键输入看源码的第一步是找到最常用的入口。绝大多数应用代码里我们获取 ViewModel 的方式是val vm ViewModelProvider(this).get(MainViewModel::class.java)或者在新版本中使用by viewModels()委托。不管哪种写法最终都绕不过ViewModelProvider这个类。打开源码它的字段很清晰public class ViewModelProvider { private final ViewModelStore mViewModelStore; private final Factory mFactory; private final CreationExtras mDefaultCreationExtras; }三个字段奠定了这个类的基本盘。mViewModelStore是 ViewModel 实例的存储容器每个 ViewModelStoreOwner 都有一个自己的 StoremFactory负责在缓存未命中时创建新的 ViewModel 实例mDefaultCreationExtras是 1.4.0 版本之后新增的用来传递一些创建时需要的外部数据比如 Application、SavedStateRegistryOwner。如果你传入的 this 是ComponentActivity它实现了ViewModelStoreOwner接口ViewModelProvider 就会从这个 Activity 身上拿到它专属的 ViewModelStore。这个 Store 是整个链接的核心——它决定了 ViewModel 存活范围的边界。同一个 Store 里相同 Class 的 ViewModel 一定只有一个实例这就是为什么两个 Fragment 共享一个 Activity 的 Store 时能拿到同一个 ViewModel 对象。2.2 get() 方法缓存命中与创建分支接着看get()的源码逻辑NonNull MainThread public T extends ViewModel T get(NonNull String key, NonNull ClassT modelClass) { ViewModel viewModel mViewModelStore.get(key); if (modelClass.isInstance(viewModel)) { return (T) viewModel; } else { // 唯一一次调用工厂创建新实例 } if (viewModel null) { // 通过 Factory 创建 } }这是整个 ViewModel 机制里最核心的检查逻辑。所有获得 ViewModel 的请求第一条路径都是先去mViewModelStore.get(key)里查——key 默认是androidx.lifecycle.ViewModelProvider.DefaultKey: canonicalName。注意这里的判断条件不仅仅是非空还要求modelClass.isInstance(viewModel)。如果你之前存的是 AViewModel第二次用 BViewModel 来 get 同一个 key类型不匹配就会用新类型覆盖之前的数据。这在业务上偶尔会用到但大多数情况下属于误用。缓存未命中时才轮到Factory.create(modelClass, extras)登场。Factory 接口定义很简单但它的实现横向跨度大AndroidViewModelFactory负责创建带 Application 参数的 ViewModelSavedStateViewModelFactory负责创建带 SavedStateHandle 的 ViewModelHilt 等依赖注入框架也有自己的 Factory 实现。自定义 Factory 也是基于这个接口。2.3 NewInstanceFactory 与 AndroidViewModelFactory构造参数差异背后的设计取舍很多人第一次看源码时会被NewInstanceFactory和AndroidViewModelFactory搞混。前者用无参构造创建 ViewModelpublic static class NewInstanceFactory implements Factory { SuppressWarnings(ClassNewInstance) Override public T extends ViewModel T create(NonNull ClassT modelClass) { try { return modelClass.newInstance(); } catch (InstantiationException e) { throw new RuntimeException(Cannot create an instance of modelClass, e); } catch (IllegalAccessException e) { throw new RuntimeException(Cannot create an instance of modelClass, e); } } }注意Class.newInstance()要求目标类必须有无参构造且不能是私有的。如果你自定义 ViewModel 只写了一个带参数构造这里是直接抛异常的。而AndroidViewModelFactory继承自NewInstanceFactory它多了一个“兜底”逻辑如果 ViewModel 继承自AndroidViewModel就会尝试反射获取带Application参数的构造函数。这个设计反映了一个取舍框架优先兼容最简单的无参构造场景把复杂度留给特定的、有明确需求的子类。AndroidViewModel本质上是我需要 Application 上下文的显式声明它会在配置变更时不销毁所以内部持有 Application 引用也不会泄漏——这个安全性正是设计它的原因。3. ViewModelStore 与 NonConfigurationInstances旋转屏幕不销毁的底层真相3.1 ViewModelStore 到底存了什么public class ViewModelStore { private final HashMapString, ViewModel mMap new HashMap(); final void put(String key, ViewModel viewModel) { ViewModel oldViewModel mMap.put(key, viewModel); if (oldViewModel ! null) { oldViewModel.onCleared(); } } final ViewModel get(String key) { return mMap.get(key); } public final void clear() { for (ViewModel vm : mMap.values()) { vm.clear(); } mMap.clear(); } }这是一个很简单的 HashMap 包装类。put时发现旧对象存在会先调用旧对象的onCleared()避免内存泄漏。clear()方法遍历所有 ViewModel 依次调用clear()同时清空 map。这个类没有继承任何生命周期组件它自己也不感知 Activity 的生命周期。谁调用它的clear()谁的 Store 就会被清空。那我们常说的旋转屏幕 ViewModel 不销毁Activity 真正 finish 才销毁是怎么实现的答案在 Activity 的非配置实例机制里。3.2 Activity 重建时 ViewModelStore 是如何被“搬运”的Android Framework 在ActivityThread执行performDestroyActivity时有一个专门处理非配置实例的步骤。核心在Activity的retainNonConfigurationInstances()方法中NonConfigurationInstances retainNonConfigurationInstances() { Object activity onRetainNonConfigurationInstance(); if (activity null mLastNonConfigurationInstances ! null) { activity mLastNonConfigurationInstances.activity; } NonConfigurationInstances nci new NonConfigurationInstances(); nci.activity activity; nci.ViewModelStore mViewModelStore; return nci; }关键就在nci.ViewModelStore mViewModelStore。Activity 在销毁前把当前的ViewModelStore塞进NonConfigurationInstances对象里存起来。重建时attach方法里又把它取回来if (lastNonConfigurationInstances ! null) { mViewModelStore lastNonConfigurationInstances.ViewModelStore; }也就是说Activity 对象确实被销毁了但 ViewModelStore 这个容器通过NonConfigurationInstances绕过了死亡过程被还给了新创建的 Activity。ImageLoader 里那种“Activity 销毁后数据还要跟过去”的困扰在系统层面就用这一小段传递解决了。这个机制是理解 ViewModel 生命周期的第一块基石不是 ViewModel 有魔力而是 Store 有搬运逻辑。Fragment 的 ViewModelStore 也类似FragmentManagerViewModel注意这个名字会在 Fragment 重建时保留 ViewModelStore 和 FragmentManagerState。3.3 onCleared() 在哪些场景下被真实调用理论上ViewModel 只在以下两种情况会被clear()Activity 真正finish()时不是配置变更Fragment 调用了setFragmentResult或 Fragment 被移除且不再保留时你手动调用viewModelStore.clear()。从源码验证这一点很简单。Activity 的onDestroy里有一句if (!isChangingConfigurations()) { mViewModelStore.clear(); }isChangingConfigurations()是决定性条件。只有配置变更导致的销毁不会清理 Store其余所有非配置变更销毁都会走清理。这个判断逻辑的严谨性直接决定了 ViewModel 依赖的边界可靠性。在实际项目中我排过一个问题A 页面 push 到 B 页面B 页面 finish 后返回 A发现 A 的 ViewModel 数据居然还在。很多人误以为这说明 ViewModel 泄漏了。实际上并没有——A 并没有 finishStore 还在ViewModel 理所当然还在。这个认知误差说明很多人对 ViewModel 的“生命周期边界”理解得不够清楚。我只能强调一句生命周期范围由 Store 的宿主决定不是由 View 的可见性决定。4. 状态恢复的进阶机制SavedStateHandle 如何配合系统后台回收4.1 为什么单单内存持有还不够如果 ViewModel 只是内存持有那系统在低内存下杀进程时会怎么样进程被杀所有堆内存随之消失这时候 ViewModel 再聪明也无济于事。为了应对进程被杀后用户返回页面还需要恢复数据的问题Google 引入了SavedStateHandle。它本质上是一个键值对容器能存储任何可序列化进 Bundle 的对象。它的特殊之处在于底层通过SavedStateRegistry把数据交给系统的onSaveInstanceState流程处理所以哪怕进程被杀下次重建时也能从 Bundle 里恢复数据。这解决了一个非常现实的问题用户填了一个长表单切到后台用了一堆其他 App系统把当前进程杀了。回来后如果只靠 ViewModel表单数据一定没了而有了 SavedStateHandle数据在进程被杀前就已经跟随 Bundle 持久化。4.2 SavedStateHandle 的创建入口与默认实现在SavedStateViewModelFactory.create()的源码里构建 ViewModel 的流程有一个关键分支// 构造 SavedStateHandle SavedStateHandle handle SavedStateHandleSupport.createSavedStateHandle( extras.get(KEY_VIEW_MODEL_STORE_OWNER), extras.get(KEY_DEFAULT_ARGS), mSavedStateRegistry);传进去的三个参数分别是 ViewModelStoreOwner、默认参数 Bundle、SavedStateRegistry。createSavedStateHandle内部会根据getSavedStateRegistry().consumeRestoredStateForKey()拿到之前保存的状态再和defaultArgs合并生成一个包含恢复数据的 SavedStateHandle 实例。如果你的 ViewModel 构造函数里声明了一个SavedStateHandle参数且使用默认的SavedStateViewModelFactory创建框架会把这个组合好的 handle 传进去。通过handle.getLiveData(name)获取的数据会自动在 SavedStateRegistry 里注册并在onSaveInstanceState时被保存。4.3 CreationExtras厂商参数传递的新通道在 ViewModel 早期版本里创建参数只有 Factory 的create(Class)要传外部依赖必须自己写工厂。1.4.0 之后引入了CreationExtras它像一张 map可以塞入键值对在创建 ViewModel 时一并传入。框架预留了一些默认 keyViewModelProvider.NewInstanceFactory.KEY_APPLICATION: 传入 Application 实例。SavedStateViewModelFactory.KEY_SAVED_STATE_REGISTRY_OWNER: 传入 SavedStateRegistryOwner。ViewModelProvider.KEY_VIEW_MODEL_STORE_OWNER: 传入 ViewModelStoreOwner。默认情况下ViewModelProvider的构造函数内部会拼装mDefaultCreationExtras把 storeOwner 和 application 自动放进去。所以即使你没有自己写 FactoryViewModel 也能拿到这些在创建时自动可用的环境参数。自定义 Factory 时常见的做法是让create(Class, CreationExtras)从 extras 里取参数而不是写在类字段里。这样每个 ViewModel 的实力边界更清晰遇到 Hilt 之类的 DI 框架时也更容易结合。5. 从“数据持有者”到“应用中枢”ViewModel 在 MVVM 架构里的角色转变5.1 早期形态只做内存缓存面向 UI 的“搬运工”早期的 ViewModel 在项目里最常见的用法是绑定一两个 LiveDataActivity 观察变化然后刷新 UI。它的角色非常单纯——我只是一个数据的持有者UI 在重建的时候保证数据还不丢。这种形态的问题在哪里过于单薄。ViewModel 变成了 View 和 Repository 之间的单向搬运管道Activity 里大量逻辑依然没有真正解耦。什么时候刷新、要不要显示加载态、错误要不要提示、用户操作的下一步流程是什么——这些逻辑全部留在了 Activity 里。ViewModel 虽然被创建了却只是个数据保险箱。5.2 现代形态状态聚合与业务调度的“中枢”当 Jetpack 生态逐步健全后ViewModel 的定位已经变了。它不再只是持有数据而是变成了 UI 与业务层之间的调度中枢——负责协调多个数据源、维护界面状态、处理用户交互事件、管理协程作用域。看一个典型场景一个二级页面的 ViewModel它同时使用了三个 Repository用户信息、订单列表、优惠券对外只暴露一个StateFlowOrderPageUiState。Activity 订阅一次collectUI 只在 uiState 变化时更新。用户在界面上点击申请售后ViewModel 负责调用对应的 Repository更新 uiState触发导航事件。整个流程里Activity 只负责组合 UI 和观察状态业务逻辑几乎全部下沉到了 ViewModel 里。这个转变的底层支撑恰恰是它源码里的两个设计viewModelScope生命周期跟随 ViewModel天然适合做协程调度onCleared()钩子可以统一释放资源。从数据持有者到应用中枢不是 ViewModel 类本身多了什么神奇方法而是架构姿势变了——开发者开始认真把 UI 状态、页面逻辑、数据流向统一收敛到一个可控的作用域里。5.3 多个 Fragment 共享同一个 ViewModel 的边界与代价activityViewModels()可以让多个 Fragment 共享同一个 ViewModel这是页面内通信的常用方案。比如一个列表页和一个详情页 Fragment 共享同一个列表 ViewModel。源码层面它们都从同一个 Activity 的 ViewModelStore 里取同一个对象所以天然支持双向通信。但共享是有代价的作用域越大生命周期越长内存占用的窗口也越大。如果共享的 ViewModel 里放了一个大 Bitmap 列表Activity 还常驻后台那这些 Bitmap 会一直占着内存。更严重的是如果 Fragment A 和 Fragment B 共享了 ViewModelA 对 ViewModel 里数据的操作可能影响 B 的界面状态造成隐式耦合。所以共享要克制尽量只共享真正的页面级数据不要把属于单页的临时状态也塞到共享 ViewModel 里。6. 源码阅读之外的实战心得几个容易翻车的细节点6.1 “Cannot create an instance of ViewModel” 异常到底在什么时候出现使用默认的NewInstanceFactory创建 ViewModel 时如果你自定义了带参构造而没有提供 Factory跑起来会直接抛RuntimeException: Cannot create an instance of ViewModel。这个异常表面是指创建失败根因就是前面源码里说的Class.newInstance()只能调用无参构造。排查的时候先排查 ViewModel 类是不是 private 的、有没有无参构造再确认是不是用了自定义 Factory。最容易踩的坑是把构造参数声明成接口类型然后忘了提供实现类比如构造参数是UserRepository但你没有自定义 Factory默认 Factory 根本不知道该怎么实现这个接口。正确做法是要么 ViewModel 保持无参构造要么写一个继承AbstractSavedStateViewModelFactory的自定义工厂。6.2 在 onCleared() 里停止一切长时间任务我见过一个项目在onCleared()里调用了一个耗时的网络请求等待逻辑。本意是优雅释放资源结果由于关联的 ViewModelStore 在某些场景下被快速连续 clear 了两次导致这段等待逻辑执行了两遍。源码里clear()方法会在 map 遍历时对每个 ViewModel 调用onCleared()如果开发者在onCleared()里又去操作了其他 ViewModel 实例时机完全不保证。最佳实践是在onCleared()里只做资源释放、协程取消、监听器注销这类同步轻量操作绝对不要执行耗时业务逻辑。协程的作用域viewModelScope已经帮你管理了协程取消不用再手动取消。6.3 自定义作用域 ViewModel 与视图复用冲突的困惑有些业务场景需要跨页面共享 ViewModel比如购物车里多个页面都要显示商品数量。有人会自定义一个 Application 级别的 ViewModelStore通过CustomStoreProvider把 ViewModel 放到全局。这种方式能解决跨页面共享但也引入了新的问题作用域变成全 App 级别的 ViewModel退到后台后数据一直驻留相当于不用了就一直没有回收。源码给出的标准解还是那句话“作用域由 Store 的宿主决定”。如果你确实需要跨页面共享优先考虑把宿主定为某个Activity或自定义的ViewModelStoreOwner而不是直接挂到 Application 上。挂到 Activity 上页面栈退出时就能自动清理挂到 Application 上你得自己找一个合适的时机调用clear()这基本等于把生命周期管理的活外包给自己——不是不行但真要当成一个工程决策来对待别漫不经心地拍脑袋。6.4 再分享一个测试视角如何在单测里给 ViewModel 构造参数写完单测的时候往往发现 ViewModel 构造函数依赖了一堆 Repository没法直接用 new 出来。官方支持跨配置变更的前提是构造不依赖 UI 上下文但 Repository 这类业务依赖是允许自定义 Factory 传入的。写单测时可以手动实例化一个ViewModelProvider.Factory在create里传一个带 mock Repository 的 ViewModel 实例然后用ViewModelProvider(store, factory).get(MyViewModel::class.java)拿到 ViewModel。注意 Store 要用真实的ViewModelStore这样测试里也能模拟真实的缓存和生命周期行为。View 层之外ViewModel 是 Android 应用架构中少数值得反复读源码的类。它本身简单但围绕它的工厂、状态保存、协程作用域、生命周期交互这些机制加起来构成了一套完整的架构基础设施。把这套机制吃透往上理解 Hilt、Flow、Jetpack Navigation 都要轻松得多。我个人的体会是阅读这些源码时不要只盯着get()和clear()这两个方法多留意那些“创建参数怎么传、缓存怎么命中、销毁时机怎么判断”的细节你在实际业务里遇到的很多疑难问题答案其实都藏在里面。
返回列表