12. ViewModel与SavedStateHandle:进程被杀后数据恢复
说实话,做Android开发这么多年,我最怕听到的一句话就是:「用户反馈填到一半的表单,切出去回了个消息,回来数据全没了。」
嗯,这其实就是进程被系统杀掉了。你想想看,用户辛辛苦苦填了十几项内容,结果因为内存紧张或者切后台太久,Activity重建了,ViewModel也跟着重建了——数据呢?全丢了。
那怎么办?今天我们就来聊聊 SavedStateHandle 这个救星。
为什么ViewModel本身不能持久化数据?
先搞清楚一个问题:ViewModel 的生命周期是跟着 Activity 走的。Activity 正常旋转屏幕,ViewModel 还在。但一旦进程被系统杀掉,ViewModel 实例就没了。
我刚开始用 ViewModel 时也踩过这个坑。当时做了一个订单填写页面,用户填了十几项,切出去接了个电话,回来页面重置了。用户直接给了个差评……从那以后,我再也不敢把「进程被杀」这件事当小概率事件了。
说白了,ViewModel 只是帮你扛住了配置变更(比如屏幕旋转),但扛不住进程被杀的「灭顶之灾」。
SavedStateHandle 是什么?
SavedStateHandle 是 Android 官方提供的一个工具,专门用来解决「进程被杀后数据恢复」的问题。它本质上是一个 Bundle 的封装,但用起来比 Bundle 舒服得多。
它的核心原理是:
- 系统在杀死进程前,会把 SavedStateHandle 里的数据保存到 Bundle 中
- 进程重建时,系统把 Bundle 传回来,SavedStateHandle 自动恢复数据
- 整个过程对开发者几乎是透明的
关键点:SavedStateHandle 是 ViewModel 的构造参数,由系统自动注入。你不需要手动创建它。
怎么用?三步走
第一步,在 ViewModel 的构造函数里接收 SavedStateHandle:
class FormViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// 用 savedStateHandle 来保存表单数据
val userName = savedStateHandle.getLiveData<String>("user_name", "")
val userPhone = savedStateHandle.getLiveData<String>("user_phone", "")
val userEmail = savedStateHandle.getLiveData<String>("user_email", "")
}
第二步,在 Activity 或 Fragment 里正常使用 LiveData:
class FormActivity : AppCompatActivity() {
private val viewModel: FormViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_form)
// 观察数据变化
viewModel.userName.observe(this) { name ->
binding.etUserName.setText(name)
}
// 用户输入时更新数据
binding.etUserName.setOnTextChangedListener { text ->
viewModel.userName.value = text
}
}
}
第三步,完事。你没看错,就这么简单。
进程被杀后,系统会自动把 savedStateHandle 里的数据存到 Bundle 里。重建时,ViewModel 拿到旧的 savedStateHandle,数据就回来了。
个人建议:SavedStateHandle 适合存「少量、关键」的数据。比如表单字段、当前选中的 Tab、滚动位置等。不要用它存大对象或列表数据,那会拖慢序列化速度。
实战:表单数据持久化
我们做一个完整的例子。假设有一个注册表单,包含用户名、手机号、邮箱、密码四个字段。
先定义 ViewModel:
class RegisterViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// 使用 SavedStateHandle 的 LiveData
val username = savedStateHandle.getLiveData<String>("username", "")
val phone = savedStateHandle.getLiveData<String>("phone", "")
val email = savedStateHandle.getLiveData<String>("email", "")
val password = savedStateHandle.getLiveData<String>("password", "")
// 验证表单
fun isFormValid(): Boolean {
return username.value?.isNotEmpty() == true &&
phone.value?.isNotEmpty() == true &&
email.value?.isNotEmpty() == true &&
password.value?.isNotEmpty() == true
}
// 提交表单
fun submit() {
// 这里做网络请求
}
}
然后在 Activity 里绑定:
class RegisterActivity : AppCompatActivity() {
private val viewModel: RegisterViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_register)
// 双向绑定(这里用简单的方式演示)
viewModel.username.observe(this) { binding.etUsername.setText(it) }
viewModel.phone.observe(this) { binding.etPhone.setText(it) }
viewModel.email.observe(this) { binding.etEmail.setText(it) }
viewModel.password.observe(this) { binding.etPassword.setText(it) }
// 监听输入变化
binding.etUsername.setOnTextChangedListener {
viewModel.username.value = it
}
// ... 其他字段同理
}
}
现在你试试:填一半表单,把 App 切到后台,然后强行杀掉进程(或者用开发者选项里的「不保留活动」)。再打开 App,表单数据还在。
我曾经踩过的坑:SavedStateHandle 默认只支持「可序列化」的数据类型。如果你存了一个自定义对象,记得让它实现 Parcelable 或 Serializable。否则进程被杀后恢复时会直接崩溃。
SavedStateHandle 的更多用法
除了 getLiveData,SavedStateHandle 还提供了其他方法:
| 方法 | 说明 |
|---|---|
| getLiveData(key) | 获取一个 LiveData,数据变化时自动保存 |
| get(key) | 直接获取保存的值,不包装成 LiveData |
| set(key, value) | 直接设置一个值 |
| contains(key) | 检查某个 key 是否存在 |
| keys() | 获取所有 key 的集合 |
我个人习惯是:能用 LiveData 就用 LiveData。因为 LiveData 自带生命周期感知,而且数据变化时 UI 会自动更新。省心。
核心知识体系
下面这张图帮你理清 ViewModel、SavedStateHandle 和进程被杀之间的关系:
避坑指南
最后分享几个我实际项目中踩过的坑:
- 不要存大对象:SavedStateHandle 底层是 Bundle,Bundle 有大小限制(约 1MB)。存大对象会导致 TransactionTooLargeException。
- 注意默认值:getLiveData 的第二个参数是默认值。进程第一次启动时,这个默认值会被使用。别把默认值设成 null 然后 UI 没做空判断,会崩。
- 不要和 Room 混用:SavedStateHandle 适合临时数据,Room 适合持久化数据。如果你需要长期保存的数据,直接用 Room,别折腾 SavedStateHandle。
- 测试要模拟进程被杀:用开发者选项里的「不保留活动」来测试,别只在模拟器上点 Home 键就以为测完了。
好了,关于 SavedStateHandle 的核心用法就这些。说白了,它就是 ViewModel 在进程被杀时的「救命稻草」。用好了,用户填到一半的表单数据再也不会丢了。