ViewModel + SavedStateHandle:彻底告别Android数据丢失的完整方案
平时在做Android开发时不知道你有没有遇到过这种场景旋转一下屏幕页面上的数据突然没了或者App退到后台被系统回收之后再切回来整个界面回到了初始状态。大部分人都知道要用ViewModel来解决旋转屏幕的数据丢失问题但真正深入之后你会发现ViewModel也保不住所有场景下的数据。今天就来聊聊ViewModel和SavedStateHandle这对组合看看怎么把它们用到极致彻底告别数据丢失。系统杀掉进程可以但我的数据不能丢。这句话是我做了几年Android开发之后对UI状态保存最深的一点体会。用户滑到一半的列表位置、填了一半的表单内容、刚输入但还没提交的搜索关键词这些一旦在进程重建后消失用户的第一反应就是卸载App。而ViewModel和SavedStateHandle存在的意义就是把这些状态从Activity/Fragment的生死轮回中剥离出来交给一个更稳定的容器去托管。这篇文章适合已经用过ViewModel、但还想进一步弄懂它原理的人也适合正在做架构选型、想避免数据和状态丢失的团队。我会从设计思路、源码原理、实战接入、常见坑点几个角度完整拆解争取让你读完就能直接拿这套思路去改造自己的项目。1. ViewModel到底在解决什么问题1.1 配置变更和数据丢失的痛点先回忆一下Android最经典的“旋转屏幕”场景。在没有ViewModel的年代Activity旋转屏幕时会经历destroy和recreateonSaveInstanceState和onRestoreInstanceState成了唯一的状态恢复手段。但这里有个非常要命的问题那些无法序列化进Bundle的对象怎么办比如正在执行的网络请求、复杂的内存缓存、需要绑定生命周期但又不应该被杀掉的数据源。我就曾经踩过这样的坑写了一个下载进度页面旋转屏幕之后进度条直接从100%掉回0%因为进度数据存在Activity成员变量里Activity一重建一切归零。后来用静态变量暂存进度虽然解决了旋转问题但进程被杀之后静态变量一样清零。这种临时方案本质上是在和Android的组件生命周期机制打游击早晚会被更复杂的场景打得体无完肤。而ViewModel的定位就是专门解决这类问题的它把UI相关的数据从Activity/Fragment中抽离出来存放到一个跟随Activity/Fragment生命周期、但又能跨越配置变更的容器里。旋转屏幕时旧的ViewModelStore不会销毁新的Activity/Fragment拿到的是同一个ViewModel实例数据自然就保住了。1.2 ViewModel的生命周期设计思路看ViewModel的设计你会发现它的生命周期锚点很巧妙。ViewModel的onCleared回调只在两种情况下触发一是Activity finish、Fragment detach也就是用户真正离开这个界面时二是ViewModelProvider的清理逻辑触发时。但旋转屏幕这种配置变更并不会走onCleared因为系统知道界面只是换了展现形式业务数据不应该跟着重新加载。从源码上看ViewModel的LRU清理机制也很值得注意。ComponentActivity里有一个ViewModelStore它内部维护了一个HashMap。当Activity配置变更时系统会把这个ViewModelStore转移到新的Activity实例上而不是重新创建一份。只有当Activity真正finish时才会调用clear方法把每个ViewModel的onCleared回调挨个触发一遍。这个设计的核心思想是区分“配置变更”和“真正关闭”。配置变更只是展示层的重建底层的数据和逻辑应该原地不动真正关闭才是彻底释放资源的时刻。理解了这一点你就知道为什么ViewModel适合做数据持有但单纯的持有还不够还得考虑进程被系统回收这种更极端的场景。1.3 作用域Store的查找机制ViewModel的查找机制里还有一个容易被忽略的点它不是一个全局单例而是按照ViewModelStoreOwner来隔离的。Activity是一个ViewModelStoreOwnerFragment是另一个ViewModelStoreOwnerNavigation的每个BackStackEntry也实现了ViewModelStoreOwner接口。这意味着你在Activity里拿到的ViewModel和在Fragment里拿到的ViewModel并不是同一个实例除非你用activityScope去共享。这个隔离机制非常关键它保证了不同界面之间的数据不会串。我在项目里经常看到新手把ViewModel当全局变量用Activity里拿一个实例改数据Fragment里又拿另一个实例监听修改结果数据不联动排查半天最后发现作用域搞错了。如果你需要在Activity和Fragment之间共享数据正确做法是通过activityViewModels()拿到Activity作用域的ViewModelFragment之间共享则通过FragmentActivity的ViewModelStore来实现。这不是ViewModel设计的问题是使用者没搞清楚作用域。2. 从源码角度拆解ViewModel的工作机制2.1 ViewModelProvider的获取链路我在读源码时把ViewModelProvider的调用链路完整追了一遍。本质上就是一次标准的工厂模式调用ViewModelProvider.get()传入Class对象先从ViewModelStore的HashMap里查如果已经有实例就直接返回没有就通过Factory的create方法反射创建。Factory的选择也有讲头。默认情况下如果你调用ViewModelProvider(this)创建它使用的是NewInstanceFactory只支持无参构造方法。但如果你的ViewModel带参比如依赖注入的数据仓库或者需要接收Application那就得自定义Factory了。常见的做法是继承AndroidViewModelFactory或者直接实现ViewModelProvider.Factory接口。每次get同一个key时只要ViewModelStore还在拿到的都是同一个实例。这个设计保证了数据的一致性不管你的Activity经历多少次重建只要了解ViewModelStore的传递机制数据就不会丢。但也带来了一个隐患如果ViewModel里持有Context引用而这个Context没有采用Application作用域就可能造成内存泄漏。这一点在后面实战部分会详细展开。2.2 ViewModelStore与配置变更的关系这里得回到ActivityThread的源码去看。当Activity重建时系统会调用Activity的onRetainNonConfigurationInstance方法把ViewModelStore塞进NonConfigurationInstances这个结构里。新的Activity创建后再从NonConfigurationInstances里把ViewModelStore取回来通过getViewModelStore方法挂到新的Activity上。这就是旋转屏幕后ViewModel数据不丢的根本原因。我最初以为是ViewModel本身做了什么魔法后来才明白就是系统在Activity重建流程里的一个状态保留机制。不过并非所有Activity重建都会走NonConfigurationInstances这条路比如设置里的开发者选项里有个“不保留活动”开关开启后Activity在后台被清理也有它自己的处理方式但正常配置变更流程就是上面说的这套。如果你没有在Application里给Activity设置configChanges系统就觉得旋转屏幕这种事归它管它会在重建流程里帮您把ViewModelStore保住。但如果你自己声明了configChangesorientation那就等于告诉系统旋转屏幕你自行处理系统也就不帮你保留ViewModelStore了。这种情况Activity不会销毁重建所以数据不会丢但如果你想让Activity重建却又声明了configChanges那ViewModelStore是会收到影响需要特别注意。2.3 为什么ViewModel能活过旋转却死在进程被杀这是很多人的直觉误区以为ViewModel能搞定所有情况。但真相是ViewModel只是持有内存中的数据当进程被系统回收时内存里的所有东西都跟着消失了。旋转屏幕不会杀进程所以ViewModel安然无恙但App退到后台系统内存不足时杀进程ViewModel还没来得及做任何持久化数据就没了。所以ViewModel解决的是配置变更场景下的数据丢失而不是进程死亡场景下的数据丢失。后者必须引入SavedStateHandle或者真正的持久化存储。SavedStateHandle的设计正是为了解决这个问题它内部使用一个SavedStateRegistry来与Activity的SavedState机制挂钩。在进程死亡前系统会调用Activity的onSaveInstanceStateSavedStateHandle就在这个时候把内部的状态数据写入Bundle。进程重建后系统恢复BundleSavedStateHandle再从Bundle里把数据捞回来。这样即使进程死了状态也能恢复数据还在那里等着你。我之前在这个问题上栽过一次列表页用ViewModel持有用户输入的筛选条件自测旋转屏幕没问题就上线了。结果用户反馈切后台再回来筛选条件经常丢失。排查后发现就是没用SavedStateHandle进程一回收就全没了。后来把用户的筛选条件改放到SavedStateHandle里同时用Bundle限制大小问题才彻底解决。3. SavedStateHandle解决进程死亡的救星3.1 为什么只需要一个带Bundle的Key-Value容器SavedStateHandle的本质其实很简单一个持有Bundle的Key-Value容器。之所以说“持有Bundle”是因为在onSaveInstanceState时会把这个Bundle写入在恢复时会重建这个Bundle。SavedStateHandle对外提供的接口包括set、get、contains、remove以及getLiveData和getStateFlow这些可观察的入口。它最省心的设计在于你不用手动处理状态保存的逻辑只需要在ViewModel的构造里接收SavedStateHandle作为参数然后想要保存什么就往里面放什么。系统会在合适的时候自动帮你完成持久化进程重建后数据自然恢复。我自己的习惯是凡是用户输入、筛选条件、列表滚动位置这类需要长期保留的数据都放SavedStateHandle凡是网络请求结果、临时缓存这种可以重新拉取的数据放在ViewModel里就行不用占着Bundle大小。这两者配合起来数据恢复的覆盖范围就非常完整了。3.2 接入方式与原理解析接入SavedStateHandle有三步第一步在build.gradle里添加依赖。最新版本的lifecycle-viewmodel-savedstate已经包含在lifecycle-viewmodel里了但为了明确最好显式加一下implementation androidx.lifecycle:lifecycle-viewmodel-savedstate:2.8.7第二步在ViewModel构造里接收SavedStateHandle参数class MainViewModel( private val savedStateHandle: SavedStateHandle ) : ViewModel() { val keyword savedStateHandle.getLiveData(keyword, ) }第三步确保ViewModelProvider的Factory是SavedStateViewModelFactory或者使用by viewModels()委托自动创建。如果你自定义了Factory需要实现SavedStateHandleCreator相关的接口否则拿到的ViewModel没有SavedStateHandle支持。原理上SavedStateHandle之所以能恢复数据是因为ViewModelProvider在创建ViewModel时会通过SavedStateRegistry注册一个BundleProvider。当Activity走到onSaveInstanceState时这个Provider会把SavedStateHandle里的内容写进Bundle。进程重建后SavedStateRegistry恢复BundleProvider再从里面取数据构造新的SavedStateHandle交给新的ViewModel旧数据也就跟着回来了。3.3 数据类型的支持与限制SavedStateHandle能存什么类型实际测试下来Bundle能接收的类型它基本都能接收基本类型、String、Parcelable、Serializable、Bundle、以及这些类型的数组和List。但注意它不能存任意对象比如一个Bitmap或者一个自定义的普通Java对象直接塞进去会抛异常。这里有一个非常容易踩的坑如果某个字段类型没有实现Parcelable或Serializable就会在写Bundle时崩溃而且崩溃的方式是在系统回调onSaveInstanceState时发生的日志里通常只看到generic framework exception不容易定位。我的建议是在把数据放进SavedStateHandle之前先确认类型是否安全。如果确实需要存非Parcelable对象要么自己实现Parcelable要么转成JSON字符串再存。另外Bundle本身有大小和序列化开销的问题。频繁往SavedStateHandle里写入大量数据或者在列表里存几千个对象可能会导致TransactionTooLargeException。虽然这个问题更多是出现在Intent传递和IPC场景但保存状态时如果Bundle过大同样的风险也存在。实际开发中要注意控制保存的数据规模。3.4 ViewModel的onCleared与SavedStateHandle的关系还有一个细节很多人不理解ViewModel的onCleared和SavedStateHandle的清理时机有什么关系。结论是没有任何关系。ViewModel的onCleared触发时SavedStateHandle不会自动清空因为SavedStateHandle的存储状态归SavedStateRegistry管而SavedStateRegistry是跟随Activity生命周期的。也就是说即使ViewModel被clear了SavedStateHandle里的数据依然存在于系统的SavedState中。下一次Activity重建时新的ViewModel会重新拿到这个SavedStateHandle数据还能恢复。这在某些场景下有用比如Fragment销毁后重新创建SavedStateHandle里的状态依然能带回来。但大多数情况下ViewModel被clear意味着界面真正关闭了SavedStateHandle里的数据也就没有意义了顺带清掉更合理。我在项目里一般把SavedStateHandle当作一个轻量级的状态快照来理解它负责在进程死亡或配置变更时保存一个可序列化的状态备份。而不是用它来做复杂的缓存或数据管理。明确了边界使用起来就不会走形。4. 从状态恢复的时机选择到响应式方案4.1 状态事件的消费时机SavedStateHandle最常用的一种使用方式是配合LiveData或者StateFlow让界面订阅状态的变化。但这里有个非常关键的问题需要搞清楚状态恢复时的数据推送跟普通的事件推送是不一样的行为。LiveData被观察到时会立即回调一次当前值。这本来是便于UI初始化的设计但如果你拿LiveData处理一次性事件比如“显示一条Toast”你会遇到一个经典问题旋转屏幕后Toast又弹了一遍。原因很简单旋转屏幕时Activity重建新的观察者注册进来LiveData把当前值立即推送给了它而系统恢复过程会重新走一遍初始化流程。这个问题的标准解法是使用Event包装类或者切换到Flow配合repeatOnLifecycle来使用。我自己在项目里已经全面转向StateFlow了流动式状态和一次性事件的分工更清晰。不过SavedStateHandle的getStateFlow在进程恢复后同样会推送当前值所以Event问题并不会因为用了Flow就自动消失还是要靠事件标识或消耗标记来处理。但要注意的是SavedStateHandle里保存的状态值在Activity因进程死亡重建时恢复可能引起界面数据与服务器数据不一致。比如用户之前输入搜索关键词后切换到后台进程死了恢复时SavedStateHandle里的关键词还在但搜索结果是之前那份缓存的。这种情况一般需要重新发起搜索或者提供一个明确的刷新入口否则用户会看到旧数据和新状态错位。4.2 为什么越来越多的项目改用StateFlow二选一的问题几乎所有做架构升级的团队都会遇到。LiveData的优势在于不需要处理协程和View生命周期自动绑定新订阅者能立即收到当前值。但LiveData也有它尴尬的地方观察者回调时不能指定线程只能靠postValue和setValue在前后台间切换多数据流的复合操作非常繁琐而Flow拥有更丰富的操作符。我最近在一个新模块里做状态管理特意对比了两种方案的开发体验。用LiveData时每当需要将两个数据源合并、去重、做防抖都要写额外的MediatorLiveData逻辑换到StateFlow之后直接用combine和debounce操作符代码量减少了约一半。再加上repeatOnLifecycle这个官方推荐的收集模式StateFlow在生命周期处理上也不逊色于LiveData。SavedStateHandle本身也提供了getStateFlow这个扩展说明官方也在推动Flow的方案。当你需要在SavedStateHandle里保存一个状态、并希望在进程重建后立即恢复时getStateFlow比getLiveData更顺手尤其是在结合ViewModel里的复杂逻辑操作时。4.3 SavedStateHandle在进程重建中的最佳实践一个稳妥的状态保存策略应该是分层的。最上层是SavedStateHandle负责保存那些跨进程重建仍然需要的关键状态中间层是ViewModel负责保存配置变更时保留的内存数据底层才是数据库或者DataStore负责真正的持久化。举个例子一个搜索页面用户输入的关键词和当前选中筛选器这两个状态必须放SavedStateHandle因为进程死了也应该恢复搜索结果列表保存在ViewModel里正常旋转屏幕不重新加载就行用户的历史搜索记录则应该存入DataStore或Room里这样才能永久保存。SavedStateHandle的设计本质上就是在帮我们在内存和持久化之间搭一座桥。它不需要你写很多存储代码但能帮你把最关键的一部分状态留存下来。很多人以为它只是生命周期组件里的一个小工具实际上它在整个状态管理架构中的位置是很重要的。5. 常见问题与排查技巧实录5.1 ViewModel内存泄漏的边界问题先说一个很容易被误解的点ViewModel里到底能不能持有Context。直接持有Activity的Context是不行的因为ViewModel存活时间比Activity长Activity销毁后ViewModel还持有它的引用就会造成泄漏。但AndroidViewModel持有的Application Context是安全的因为Application生命周期是整个进程。我在实践中的做法是能不用Context就不用Context需要Repository时通过构造传Application或者使用Hilt这类依赖注入框架来管理。如果非得在ViewModel里使用Context做资源获取通过applicationContext并确保使用完毕及时释放。还有一个被忽视的内存泄漏入口ViewModel里持有观察者的引用比如Flow的collectJob。ViewModel被clear时协程Job不会自动取消需要通过viewModelScope来自动管理协程作用域这样onCleared时会自动cancel所有协程。如果你在ViewModel里用GlobalScope启动协程那内存泄漏就是必然的。5.2 不要在SavedStateHandle里存大数据SavedStateHandle虽然方便但不是万能存储。因为它的底层会序列化进BundleBundle序列化是有限制的大体积数据不仅拖慢保存和恢复的速度还可能引发TransactionTooLargeException。特别是当你的SavedStateRegistry作为Fragment的SavedState传递时跨进程传递的Binder事务其实是有大小限制的Android大约1MB左右但这并不是一个严格精确的数字。我做过一次错误的示范在ViewModel里用SavedStateHandle保存了一个几万条数据的用户列表结果两个问题接踵而来一是恢复时耗时特别长启动页面明显卡顿二是在低内存设备上直接崩溃日志里能看到TransactionTooLargeException。后来把用户列表改从数据库加载SavedStateHandle里只存一个查询条件问题就消失了。所以一个基本经验是SavedStateHandle只用来存轻量级的状态数据比如ID、布尔值、短字符串、小集合。需要保存大对象时用Room或者DataStoreSavedStateHandle里只存一个可以被加载的凭据。5.3 配置变更时ViewModelStore没保住的情况前面说了configChanges的影响这里补充一个比较隐蔽的场景如果你想在Activity里声明configChanges来阻止系统重建那么ViewModelStore不会收到影响因为Activity根本没重建。这本身不是问题但如果你同时使用了ActivityResultContracts或者其他依赖重建机制的功能系统调用顺序上可能会出现状态不一致。另一个情况是多个Fragment叠在一个Activity里Fragment各自用activityViewModels()共享ViewModel。这个共享机制在Fragment配置变更时没问题但如果你在Fragment的onDestroyView里清除了ViewModelStore比如手动调用viewModelStore.clear()就会导致Activity重建后ViewModel数据丢失。这种情况属于人为破坏生命周期要尽量避免。最后用FragmentTransaction的replace切换Fragment时如果不调用addToBackStack旧Fragment会被销毁viewModelStore也随之清空。如果想让Fragment切换后数据不丢要么保留Fragment实例要么将数据提到Activity作用域的ViewModel里。5.4 结合热词和Android 16适配的思考当前正好处于Android 16适配的窗口期如果你做的是跨端应用或者SDK开发需要留意的是Android 16对生命周期组件行为的一些调整尤其是进程优先级和后台限制方面的变化。ViewModel本身的设计原则没有变但SavedStateHandle作为状态恢复的一个关键点在系统对后台进程回收策略越来越严格的背景下重要性只会更高。还有一个容易被忽略的点Android 16之后系统对前台服务的启动限制更严格这对后台数据刷新有影响。如果ViewModel依赖前台服务维持数据就需要在适配时重新考虑数据同步的时机和方式。好消息是通过SavedStateHandle保存的状态在进程重建后能够可靠恢复这项能力不会受到新系统的影响。另外ActivityResultContracts在Jetpack里的使用越来越普遍它内部也涉及生命周期绑定和结果传递。如果你在ActivityResultCallback里更新ViewModel数据要注意顺序先恢复ViewModel数据再执行结果回调否则可能出现用旧数据覆盖新结果的问题。这一点我在适配新版本时吃过亏这里一并提醒大家。6. 实战技巧一套更稳的状态管理方案6.1 状态分层设计我把Android端的状态管理总结成四层交互状态、界面状态、业务状态、持久状态。交互状态指用户正在进行的输入或手势比如正在拖动的位置这类状态通常放在LocalVariable里不跨场景保留界面状态指展示所需的全部数据放ViewModel业务状态指需要跨UI层共享的数据一般放Repository持久状态才是真正需要落盘的数据放DataStore或Room。SavedStateHandle处在这四层中的特殊位置它不负责业务逻辑只负责在进程死亡时把界面状态的关键部分保留下来。一个简单的判断标准是如果用户中断操作后几分钟再回来界面上应该保持什么这些保持项就应该放进SavedStateHandle。在实现时我会给ViewModel定义两类字段一类是普通MutableStateFlow用于不需要跨进程重建的临时状态一类是SavedStateHandle的StateFlow用于需要进程重建恢复的状态。两类状态各司其职代码的意图一眼就能看懂。6.2 结合Navigation和依赖注入如果你用了Navigation组件会发现Navigation本身提供了SavedStateHandle的集成每个BackStackEntry都有自己的SavedStateHandle。这样在使用SavedStateHandle时能精确地把状态保存到某一个destination而不是全部保存在Activity级别。另外在依赖注入一个数量级的项目里ViewModel的创建往往交由DI容器管理。拿Hilt为例Hilt的SavedStateHandle支持是通过HiltViewModel注解和SavedStateHandle构造参数自动完成的。如果你自定义Factory一定不要忘记处理SavedStateHandle参数的注入否则运行时会报IllegalArgumentException而且这个错误信息没有明显的提示排查成本很高。我遇到的一个典型案例是项目里从手写工厂迁移到Hilt原来的ViewModel没有处理SavedStateHandle接口一直正常。后来在某个页面加了一个SavedStateHandle参数重新编译后启动就崩溃日志只显示“has no zero argument constructor”找了半天才发现是Factory没走Hilt的ViewModelFactory而是自己new的。细节问题但会让整个页面直接白屏。6.3 恢复时机与前台可见性即使有了SavedStateHandle状态恢复的时机也仍然要谨慎。Activity在进程死亡重建后onCreate里拿到的SavedStateHandle数据是旧数据此时界面不应该直接根据旧数据做全量更新而是应该根据数据的时效性和业务需求决定是否刷新。比如一个新闻列表进程恢复时显示旧列表没问题但应该触发一次静默刷新保证数据不过期。还有一种情况SavedStateHandle恢复的数据与远端数据不一致比如用户修改了某个设置项后立刻切后台进程被杀前SavedStateHandle保存的是未提交的修改但服务端还没有收到。恢复后如果直接按SavedStateHandle的数据渲染用户会以为修改成功了实际上服务端并没更新。这种场景需要把SavedStateHandle和缓存的远端状态做对比必要时提示用户重新提交或者回滚。7. 别忘了onSaveInstanceState的存在7.1 onSaveInstanceState与SavedStateHandle的分工很多人习惯了用ViewModelSavedStateHandle之后把Activity的onSaveInstanceState抛到脑后。这种做法我觉得有点可惜。onSaveInstanceState和SavedStateHandle虽然看起来功能重叠但本质还是有区分Activity的onSaveInstanceState保存的是View层级的状态比如EditText的内容、ScrollView的位置、RecyclerView的滚动位置这些在不使用ViewBinding时其实是由系统自动保存和恢复的。SavedStateHandle更偏向为业务状态提供一个存取通道。两者配合可以覆盖更完整的状态恢复场景。我在实际项目中不会刻意去重写onSaveInstanceState除非有一些View不会自动保存的状态比如自定义View里的某个标志位。一般情况下用SavedStateHandle已经覆盖了业务层的状态View状态交给系统机制处理就够用了。7.2 SavedStateHandle与Fragment之间状态保持一致如果你在Fragment里用了SavedStateHandle要注意Fragment的SavedState机制和Activity是独立的也就是说Fragment能够保存自己的状态但是依赖Activity的onSaveInstanceState时机。如果Activity因配置变更重建Fragment的SavedState也会跟着保存和恢复这个流程内部其实非常精密。有一个容易出问题的点Fragment在onDestroyView中释放了View引用但ViewModel和SavedStateHandle还在。如果在onDestroyView之后往SavedStateHandle里写数据理论上是可以的但如果你用这些数据驱动UI更新就会操作已经销毁的View导致空指针或崩溃。所以我建议SavedStateHandle的写入尽力放在生命周期onStop之前读取则放在onViewCreated里做。8. 项目迁移过程中的实操复盘8.1 从传统写法迁移到ViewModelSavedStateHandle的步骤如果你的项目还在用Activity保存状态的老写法想迁移到ViewModelSavedStateHandle我建议分三步走。第一步为每个承载状态的Activity/Fragment新建ViewModel类把成员变量里的业务数据搬进去只搬数据不搬UI逻辑第二步把需要跨进程重建的状态提取到SavedStateHandle里初始化时从handle读取更新时写入handle第三步把UI层改成观察ViewModel暴露的LiveData或StateFlow所有的数据变更通过ViewModel提供的方法触发。这一步走完你会惊喜地发现Activity的代码量骤减职责清晰了很多同时状态恢复的适用性也大幅提升。但也要注意迁移过程中如果有带参构造的ViewModel需要同步处理好Factory。我经历过一个12万行代码的老项目迁移整个过程最大的阻力不是技术而是团队的心态——大家习惯了在Activity里写逻辑对ViewModel的抽象有抵触。后来做一个Demo验证了旋转屏幕和进程死亡两种场景下数据都不丢团队自己就有了动力。再后来我们把迁移的checklist写进了Code Review规范新代码强制要求使用ViewModelSavedStateHandle老代码按模块逐步迁移。8.2 检查清单与性能验证迁移完成后最好有一个标准的验证清单旋转屏幕输入数据是否保留滚动位置是否保留切换深色模式状态是否保持开发者选项开启“不保留活动”后返回页面状态是否恢复用adb命令模拟进程死亡后返回状态是否恢复# 模拟进程被系统杀死的一种方式不保留后台Activity adb shell settings put global always_finish_activities 1 # 或通过am kill命令直接杀进程 adb shell am kill com.example.yourapp如果第4项也通过了说明SavedStateHandle的接入是成功的。这里还要提醒一下模拟进程死亡时别忘了考虑任务栈的情况有些设备从最近任务列表恢复时并不重建Activity而是直接复用已有实例这会影响你测试的结果。最可靠的做法是用always_finish_activities开关关掉后台保存然后切后台再切回来强制走重建流程。8.3 性能与包体积影响ViewModel和SavedStateHandle对性能的影响基本可以忽略。ViewModel本身是一个轻量对象无构造逻辑时创建开销很小SavedStateHandle的序列化成本取决于保存的数据量只要你控制好不存储大数据保存和恢复的耗时都在毫秒级别。包体积方面androidx.lifecycle已经是一个大而全的库如果你项目里用到的lifecycle相关功能很多可以考虑用R8压缩清理无用类。默认配置下ViewModelSavedStateHandle相关代码能很好地被混淆和压缩不会成为包体积增长的元凶。与之相对的如果你为了让SavedStateHandle能存更多类型而引入序列化框架那反而可能增加不小的体积成本要谨慎评估。9. 分享一点个人的心得体会做Android开发这几年我觉得最能拉开水平差距的往往不是用了多新奇的框架而是对状态保存这套基础机制的理解深浅。ViewModel和SavedStateHandle都不是什么新东西但它们组合起来能解决掉绝大多数用户可感知的数据丢失问题。我个人的体会是状态保存这种事不要等到线上出了事故才重视。设计一个新页面时先问自己三个问题旋转屏幕数据会不会丢进程被杀数据会不会丢用户清理后台数据会不会丢任何一个问题回答不出来说明状态管理方案还没到位。而ViewModelSavedStateHandle是解决这三个问题的最低成本方案。最后再分享一个小技巧当你在调试一个状态恢复的疑难问题时不要只看logcat可以用Android Studio自带的Layout Inspector对比恢复前后界面上的实际数据这种可视化的方式往往能更快定位是数据没保存还是保存了但UI层没有正确读取还是数据被后续逻辑覆盖了。这套排查思路配合今天讲的ViewModel和SavedStateHandle的原理很多状态问题都能快速找到根因。