Skip to content

状态持久化、内存恢复与分层恢复策略 ​

回到总览:状态管理、路由、架构
相关模块: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 校验与原子写保障。

底层机制 ​

1. 状态三分层架构与生命周期映射 ​

对应 Lab:state-persistence-demo · StatePersistenceDemoActivity.kt

只画"内存态 → 恢复态 → 持久态"三层箭头还不够,因为真实事故并不是问"你知不知道三层",而是问:某个具体状态到底会死在哪个边界、应该放哪层、恢复时先从哪里拿。

状态类型、存活边界、推荐存放层与恢复入口按事故导向展示表单输入、列表筛选、分页游标、滚动位置、草稿正文、登录 token 等状态应放在内存态、恢复态或持久态,以及旋转、进程死亡、冷启动时应从哪里恢复。事故导向状态分层图从左到右回答四件事:这是什么状态 → 它死在哪个边界 → 应该放哪层 → 出事后先从哪里恢复1. 状态类型表单输入中间态输入框内容、校验提示、按钮 loading列表筛选条件 / Tabquery、tabIndex、排序方式分页游标 / 当前详情 IDcursor、itemId、draftId滚动位置 / 局部展开态scrollOffset、展开折叠、焦点草稿正文 / 离线缓存草稿全文、列表缓存、业务实体登录 token / 用户设置auth、dark mode、feature flags2. 存活边界只需抗重组 / 旋转进程还在即可页面重建后能接着画 UI要抗进程死亡但只恢复轻量锚点别把大对象塞 Bundle要抗冷启动 / 重装前必须落磁盘下次启动仍要在3. 推荐存放层内存态ViewModel / StateFlow / remember / Provider放可重算 UI、临时组合态、页面内缓存恢复态SavedStateHandle / Bundle / rememberSaveable只放 ID、页码、筛选条件、轻量输入持久态DataStore / Room / 文件 / 本地数据库放跨启动事实、草稿正文、离线业务数据4. 恢复入口配置变更 / 页面重建直接从 ViewModel、rememberSaveable 或局部缓存恢复进程死亡后首屏恢复先读 Bundle / SavedStateHandle 锚点再用 itemId / draftId 回补完整数据冷启动 / 重新进入应用从 DataStore / Room 读盘,再重建内存态不要指望 ViewModel 或 Bundle 保住重量数据恢复态常只存锚点,完整数据仍要回磁盘判断口诀:能重算的留内存,只需锚点的进 SavedStateHandle,要跨启动保真的落 DataStore / Room;Bundle 不背大对象,磁盘不背一次性 UI 噪音。
  • 读图方式:先在第一列找到"出事故的那个状态",再顺着箭头看它的死亡边界、推荐层级与恢复入口。
  • 一句话结论:恢复态不是"第二份内存缓存",而是给进程死亡留的轻量锚点;真正的大对象、草稿正文、业务事实仍应回到持久层。

三层状态机制对比 ​

状态层级存储载体存储容量限制生命周期存活范围典型存储内容
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 对照 ​

存储层级AndroidFlutterWeb 前端
内存态ViewModel / StateFlowRiverpod Provider / BLoCPinia / Vuex / React State
恢复态SavedStateHandle / rememberSaveablePageStorage / RestorationManagersessionStorage
持久态DataStore / Room / MMKVshared_preferences / hive / sqflitelocalStorage / IndexedDB

常见场景 ​

1. 模拟“开发者选项 - 不保留活动 (Don't Keep Activities)”排查恢复 Bug ​

在 Android 开发者选项中开启“不保留活动”,离开 App 切后台即刻触发 Activity 销毁;再次切回验证 SavedStateHandle 是否能完美还原当前 UI 页面。

常见误配、事故后果与排障 ​

1. 事故:SharedPreferences .apply() 导致主线程 ANR ​

  • 误配原因:以为 sp.edit().putString().apply() 是异步的就在主线程高频调用。
  • 后果:虽然 apply() 确实将磁盘写入抛给了后台线程,但在 Activity onStop() / onDestroy() 时,系统 QueuedWork 会强制等待 apply() 的后台磁盘写入完成;若此时磁盘繁忙,主线程被挂起触发 ANR!
  • 排障与修法:全量迁移至基于 Kotlin Flow 的 Jetpack DataStore。

与相近概念对比 ​

特性SharedPreferencesJetpack 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

复习检查题 ​

  1. 在状态持久化设计中,为什么不能直接用 SharedPreferences 做高频数据的写入?

    答:因为 SharedPreferences 的 get() 方法是同步阻塞的,在主线程第一次读取时如果文件较大容易造成卡顿;而 apply() 方法虽然是在后台子线程写入,但在 Activity 销毁或切后台(onStop / onDestroy)时,系统的 QueuedWork 会强行等待未完成的磁盘写入动作完成,直接造成主线程阻塞甚至引发 ANR。

  2. 状态三分层设计中,SavedStateHandle(恢复态)与 DataStore(持久态)的分工分别是什么?

    答:SavedStateHandle 属于恢复态,数据存储在内存与 Binder 的 Bundle 暂存区中,专门负责存放轻量级的 UI 引用索引(如当前选中的 tab 下标、搜索关键字 ID、当前列表的 Offset 游标),用于抵御进程被杀后的无感 UI 节点恢复;而 DataStore 属于持久态,数据存储在磁盘上,负责存放重量级的业务数据、用户 Auth 凭证、离线草稿等长久数据。

速记 ​

  • 三分层原则:内存态存 UI,恢复态存 ID,持久态存磁盘。
  • 彻底抛弃 SP:SharedPreferencesQueuedWork 易引发 ANR,全面拥抱 DataStore。
  • 验证手段:开启“不保留活动”,主动验证进程被杀后的状态恢复逻辑。

站点构建时间:2026/8/24 23:43:17