ViewModel 的 SavedStateHandle,进程杀死后数据还在吗
ViewModel 的 SavedStateHandle,进程杀死后数据还在吗
最近在重构一个订单填写页时,我把页面里的表单状态全部迁进了 ViewModel 的 SavedStateHandle。本地开发时开着开发者选项里的「不保留活动」测了一下午,切后台、拍照返回、权限请求,数据都能恢复,觉得稳了。结果交给 QA 做 Monkey 测试,在低内存设备上跑了一夜,第二天给我看日志:进程被系统杀死后恢复,部分字段丢了,而且是随机丢。这个「随机」让我很不爽,说明我对 SavedStateHandle 的持久化边界理解有误,必须把它彻底拆清楚。
SavedStateHandle 不是独立存储,它只是 Bundle 的代理层
很多人以为 SavedStateHandle 是 ViewModel 自带的一个「轻量级磁盘缓存」,进程死了它也能从某个神秘角落把数据捞回来。这是个误会。SavedStateHandle 本质上只是系统提供的 onSaveInstanceState(Bundle) 机制的一个 Jetpack 封装。
从 Jetpack 2.5.0 开始,你通过默认的 ViewModelProvider 获取 ViewModel 时,SavedStateHandle 已经作为默认参数注入。它的内部实现依赖两个核心类:SavedStateRegistry 和 SavedStateHandleController。当你调用 savedStateHandle.set("key", value) 时,值并没有立刻写到磁盘,而是暂存在一个内部 Map<String, Any?> 里。只有当系统触发 onSaveInstanceState 时,SavedStateRegistry 才会遍历所有注册的 SavedStateProvider,把数据序列化进一个 master Bundle。这个 master Bundle 随后被系统 Activity Manager 保留在内存中,如果进程没被杀死,下次配置变更时直接复用;如果进程被杀死,重启后系统会通过 ActivityClientRecord 把这个 Bundle 传回给你的应用。
关键点在于:Bundle 必须被成功写出,且进程重启后能成功读回。这里每一步都可能失败。Jetpack lifecycle-viewmodel-savedstate:2.6.1 的源码里,SavedStateHandle 的 savedStateProvider 长这样:
private val savedStateProvider = SavedStateRegistry.SavedStateProvider {
regular.values.forEach { ... }
// 把 Map 转成 Bundle
}如果你存的是普通 Parcelable 或基本类型,序列化通常没问题。但如果你塞了一个不能序列化的 lambda,或者一个持有 Context 引用的匿名类,进程重启后恢复时就会直接崩溃。更隐蔽的是,如果你存了一个自定义 Parcelable,但恢复时类加载器找不到这个类(比如动态功能模块还没加载),系统不会抛明显异常,而是静默跳过这个 key,导致你 getStateFlow("key", default) 拿到的是 defaultValue,仿佛数据从未存在过。
系统杀进程与用户划掉,完全是两套逻辑
SavedStateHandle 的数据能不能在进程杀死后幸存,首先取决于进程是怎么死的。
当系统因为低内存(LMK, Low Memory Killer)回收你的后台进程时,Android 框架在销毁 Activity 之前,通常会调用 onSaveInstanceState。从 Android P(API 28)开始,这个行为变得更可靠:即使 Activity 已经进到后台 onStop,系统在决定杀进程前仍会尝试保存状态。但这个「尝试」不带有强制契约。如果系统内存已经紧张到连回调一次 Activity 生命周期都嫌奢侈,它会直接发送 SIGKILL,此时你的 savedStateProvider 根本没有执行机会,Bundle 里就是空的。
而用户从最近任务列表划掉应用(Swipe away),行为又不一样。传统上,这被视为用户主动 finish 任务,系统不会调用 onSaveInstanceState。在 Android 12(API 31)之前,这是铁律。Android 12 之后,Google 对后台任务管理做了调整,但划掉应用仍然大概率不会触发状态保存。这意味着 SavedStateHandle 在这种场景下是一定会丢数据的。丢数据不算是 bug,而是符合设计预期:用户明确表达了「我不想让这个应用继续存在」的意图。
这里还有一个国产 ROM 的特殊变量。部分 OEM 在定制的系统服务里加了激进的后台清理策略,会在屏幕锁定时直接清空缓存进程,并且为了节省 CPU 时钟,跳过 onSaveInstanceState 的调用。我在一台搭载某定制 Android 13 的设备上实测过:通过 adb shell am send-trim-memory 把应用推到 RUNNING_CRITICAL,再触发系统回收,恢复后 SavedStateHandle 里的一个复杂对象确实消失了,而同一台机器上用 AOSP 模拟器测试时数据却能恢复。这种碎片化让「进程杀死后数据还在吗」这个问题没有统一答案。
「不保留活动」是个不诚实的测试工具
开发者选项里的「不保留活动」(Don't keep activities)害人不浅。它会在 Activity 走 onStop 之后立即调用 onDestroy,但注意:它仍然会完整地走一遍 onSaveInstanceState -> onDestroy 的流程。也就是说,你的 SavedStateHandle 里的 savedStateProvider 会被执行,数据被写进 Bundle,然后 Activity 被销毁。当你切回来时,系统用那个 Bundle 重建 Activity,SavedStateHandle 恢复数据,一切看起来完美无缺。
但真实的低内存杀进程根本不是这个剧本。真实场景下,系统可能是在你应用处于后台十分钟后,突然因为内存水位达到临界值而发送 SIGKILL。你的进程没有任何预警,主线程不会收到 onTrimMemory 之后的 onStop,更不会有 onSaveInstanceState。为了模拟这个场景,你不能依赖「不保留活动」。
相对真实的测试手段有几种。一种是写一个 Native 内存泄漏,让系统的 lmkd 真正工作起来,观察应用在后台被回收时的行为。另一种是通过 adb shell cmd activity memory-factor critical