Appearance
状态持久化、内存恢复与分层恢复策略
回到总览:状态管理、路由、架构
相关模块:ViewModel 架构、SavedStateHandle 与进程被杀恢复 · 移动端与服务端数据库、Redis 缓存体系总览
一句话定义
状态持久化与恢复策略是指将应用状态清晰划分为内存态 (Transient Memory State)、恢复态 (Saved State / Process Restoration) 与 持久态 (Persistent Disk Storage) 三层架构,确保应用在经历屏幕旋转、退后台进程被杀或断电重启时均能以最低代价无感恢复的工程设计规范。
代码索引
| 主题 | Lab 说明 | 源码 |
|---|---|---|
| state-persistence-demo | 恢复态 / 内存态 / 持久态三分层,验证筛选条件、分页游标、草稿正文的存放边界 | StatePersistenceDemoActivity.kt |
为什么需要
- 为什么不能把所有 UI 状态(包括长列表列表数据)统统存入
onSaveInstanceState/SavedStateHandle?- 一句话答:
SavedStateHandle的底层是 Binder 传递的 Bundle 内存空间,上限仅约 1MB;盲目塞入海量列表或大图像会导致TransactionTooLargeException崩溃,且增加序列化开销。
- 一句话答:
- 状态持久化的“三分层设计”原则是什么?
- 一句话答:内存态存 UI 渲染与临时 ViewModel 数据;恢复态存轻量级的 ID、选中 Key 和页码(抵御进程被杀);持久态(SQLite/DataStore)存核心业务数据与离线草稿。
- 为什么 Jetpack DataStore 能够彻底替代传统 SharedPreferences?
- 一句话答:SharedPreferences 的
get()在主线程做同步磁盘 I/O 极易引发 ANR,且apply()存在隐式主线程等待;DataStore 基于 Kotlin Flow / Coroutines 实现了 100% 异步非阻塞、强类型 Protobuf 校验与原子写保障。
- 一句话答:SharedPreferences 的
底层机制
1. 状态三分层架构与生命周期映射
对应 Lab:state-persistence-demo · StatePersistenceDemoActivity.kt
只画"内存态 → 恢复态 → 持久态"三层箭头还不够,因为真实事故并不是问"你知不知道三层",而是问:某个具体状态到底会死在哪个边界、应该放哪层、恢复时先从哪里拿。
- 读图方式:先在第一列找到"出事故的那个状态",再顺着箭头看它的死亡边界、推荐层级与恢复入口。
- 一句话结论:恢复态不是"第二份内存缓存",而是给进程死亡留的轻量锚点;真正的大对象、草稿正文、业务事实仍应回到持久层。
三层状态机制对比
| 状态层级 | 存储载体 | 存储容量限制 | 生命周期存活范围 | 典型存储内容 |
|---|---|---|---|---|
| 1. 内存态 | ViewModel / Memory Cache | 无限制 | 配置变更 (屏幕旋转) 存活;进程被杀丢失 | 复杂UI State、加载数据列表、临时表单输入 |
| 2. 恢复态 | SavedStateHandle / Bundle | < 1MB | 跨进程被杀存活;卸载/显式退出丢失 | 选中的 Tab 索引、未提交输入 ID、当前页码 |
| 3. 持久态 | DataStore / Room / MMKV | 磁盘文件容量 | 永久保存;直至应用被卸载或清理缓存 | 用户登录 Token、离线草稿、全局系统设置 |
2. 进程被杀 (Process Death) 触发与无感恢复时序
图注补充:Bundle: SavedStateRegistry (OS 托管);Disk: DataStore / SQLite 磁盘;Bundle: 2. 实时更新 SavedStateHandle (存 userId, draftId);Disk: 3. 异步写入离线草稿 (DataStore);Disk: 8. 用 draftId 异步读取 Disk 完整草稿内容;User: 9. 界面无感还原到退后台前的状态!。
代码示例与 DataStore 异步持久化
kotlin
// 使用 Preferences DataStore 进行强类型异步设置项读写
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "user_settings")
class SettingsRepository(private val dataStore: DataStore<Preferences>) {
private val DARK_MODE_KEY = booleanPreferencesKey("dark_mode")
// 1. Flow 响应式读取 (异步非阻塞)
val isDarkMode: Flow<Boolean> = dataStore.data
.catch { exception ->
if (exception is IOException) emit(emptyPreferences())
else throw exception
}
.map { preferences -> preferences[DARK_MODE_KEY] ?: false }
// 2. 协程原子写入
suspend fun setDarkMode(enabled: Boolean) {
dataStore.edit { preferences ->
preferences[DARK_MODE_KEY] = enabled
}
}
}Android / Flutter / Web / Backend 对照
| 存储层级 | Android | Flutter | Web 前端 |
|---|---|---|---|
| 内存态 | ViewModel / StateFlow | Riverpod Provider / BLoC | Pinia / Vuex / React State |
| 恢复态 | SavedStateHandle / rememberSaveable | PageStorage / RestorationManager | sessionStorage |
| 持久态 | DataStore / Room / MMKV | shared_preferences / hive / sqflite | localStorage / IndexedDB |
常见场景
1. 模拟“开发者选项 - 不保留活动 (Don't Keep Activities)”排查恢复 Bug
在 Android 开发者选项中开启“不保留活动”,离开 App 切后台即刻触发 Activity 销毁;再次切回验证 SavedStateHandle 是否能完美还原当前 UI 页面。
常见误配、事故后果与排障
1. 事故:SharedPreferences .apply() 导致主线程 ANR
- 误配原因:以为
sp.edit().putString().apply()是异步的就在主线程高频调用。 - 后果:虽然
apply()确实将磁盘写入抛给了后台线程,但在 ActivityonStop()/onDestroy()时,系统QueuedWork会强制等待apply()的后台磁盘写入完成;若此时磁盘繁忙,主线程被挂起触发 ANR! - 排障与修法:全量迁移至基于 Kotlin Flow 的 Jetpack DataStore。
与相近概念对比
| 特性 | SharedPreferences | Jetpack DataStore (Preferences) | Jetpack DataStore (Proto) |
|---|---|---|---|
| API 调用 | 同步阻塞 API | 基于 Flow 异步响应式 | 基于 Flow 异步响应式 |
| 主线程安全 | 否(容易引发 ANR) | 是(调度至 Coroutine IO) | 是(调度至 Coroutine IO) |
| 数据强类型 | 否(仅基础 Key-Value) | 否(Key-Value 键值对) | 是(Schema 编译生成对象) |
| 数据损坏容错 | 弱 | 有 CorruptionHandler 容错 | 有 CorruptionHandler 容错 |
对应实验
| Lab | 说明 | 源码 |
|---|---|---|
| state-persistence-demo | 三层状态存储、进程恢复与 DataStore 草稿回补 | StatePersistenceDemoActivity.kt |
复习检查题
在状态持久化设计中,为什么不能直接用 SharedPreferences 做高频数据的写入?
答:因为 SharedPreferences 的
get()方法是同步阻塞的,在主线程第一次读取时如果文件较大容易造成卡顿;而apply()方法虽然是在后台子线程写入,但在 Activity 销毁或切后台(onStop/onDestroy)时,系统的QueuedWork会强行等待未完成的磁盘写入动作完成,直接造成主线程阻塞甚至引发 ANR。状态三分层设计中,
SavedStateHandle(恢复态)与DataStore(持久态)的分工分别是什么?答:
SavedStateHandle属于恢复态,数据存储在内存与 Binder 的 Bundle 暂存区中,专门负责存放轻量级的 UI 引用索引(如当前选中的 tab 下标、搜索关键字 ID、当前列表的 Offset 游标),用于抵御进程被杀后的无感 UI 节点恢复;而DataStore属于持久态,数据存储在磁盘上,负责存放重量级的业务数据、用户 Auth 凭证、离线草稿等长久数据。
速记
- 三分层原则:内存态存 UI,恢复态存 ID,持久态存磁盘。
- 彻底抛弃 SP:SharedPreferencesQueuedWork 易引发 ANR,全面拥抱 DataStore。
- 验证手段:开启“不保留活动”,主动验证进程被杀后的状态恢复逻辑。