## 3. DataStore 的历史与设计初衷:替代 SharedPreferences、键值对与 Protobuf、数据流响应式 ### 3.1 诞生背景:为什么 SharedPreferences 不够用了? 在 Android 早期,**SharedPreferences(SP)** 是唯一官方提供的轻量级键值对存储方案。它基于 XML 文件,每次写入都会触发整个文件的序列化与磁盘 I/O。随着应用规模增长,SP 的缺陷逐渐暴露: - **同步阻塞**:`apply()` 虽是异步,但实际依然在主线程排队;`commit()` 直接阻塞主线程。 - **无类型安全**:get/set 方法返回 `Object`,取数据时需手工强转,易引发 `ClassCastException`。 - **进程安全缺失**:多进程操作时,`MODE_MULTI_PROCESS` 只是标记,实际不加锁,数据容易错乱。 - **性能波动大**:文件变大后,每次写入都要重写整个 XML,CPU 和 IO 消耗激增。 Google 在 2019 年 I/O 大会上正式推出 **DataStore**,明确其定位:**替代 SharedPreferences 的现代数据存储组件**,并解决上述所有痛点。 ### 3.2 从“键值对”到“类型安全”:两种实现 DataStore 提供两种数据组织方式,核心差异如下: | 对比维度 | **Preferences DataStore** | **Proto DataStore** | |----------|---------------------------|----------------------| | 数据格式 | 纯键值对,类似 SP | 遵循 Protobuf schema 的结构化对象 | | 类型安全 | 弱(需手工 `int.toPreferences()`) | 强(编译期生成代码,类型由 .proto 定义) | | 修改方式 | 使用 `PreferenceKeys` 做增删改 | 使用 `updateData` 传入 Data 对象的 Builder | | 多字段关系 | 无约束,字段松散 | 自带嵌套、枚举、列表等结构性约束 | | 二进制大小 | 小(JSON-like 转 Proto 编码) | 固定体积,序列化效率高 | > **Protobuf**(Protocol Buffers)是 Google 的语言中立、平台中立的数据交换格式,以 `.proto` 文件定义数据结构,编译器自动生成 Java/Kotlin 类。DataStore 的二进制存储层正是基于 Protobuf 协议。 ### 3.3 核心设计思想:数据流响应式 无论是 Preferences 还是 Proto 实现,DataStore 的数据读取都统一采用 **kotlinx.coroutines.flow** 的 `Flow` 流。这意味着: - **订阅即获得当前值**:每次读取都自动注册监听器,数据变化立即推送到订阅者。 - **天然适配协程**:所有读写操作都挂起在协程中,绝不阻塞主线程。 - **响应式更新 UI**:结合 LiveData 或 StateFlow,可以做到“数据变 → UI 自动刷新”。 下面这张对比图直观展示了两者的架构差异: SharedPreferences vs DataStore 架构对比 SharedPreferences App 直接读写 XML 文件 同步提交(commit / apply) 主线程阻塞 → ANR 风险 DataStore 协程挂起 + Flow 响应式流 Protobuf 二进制序列化 类型安全 + 编译期校验 App 左:同步、非类型安全、易阻塞  右:异步、类型安全、响应式 ### 3.4 生活类比:纸质账本 vs 电子台账 - **SharedPreferences** 好比一本 **纸质账本**:每次记账要重写整本账单(全量写入),别人同时翻看时容易撕错页(线程不安全),字迹模糊就出错(无类型校验),而且必须等墨干才能翻页(阻塞)。 - **DataStore** 则像 **电子台账系统**:每次操作只增改对应行(增量写入),多人可同时查看实时版本(Flow 流),有固定的数字格式(Protobuf 约束),修改后所有屏幕自动刷新(响应式)。 ### 3.5 常见误区(避坑指南) | 误区 | 正解 | |------|------| | DataStore 一定能替代所有 SharedPreferences 场景 | 只适用于键值类配置数据;如果数据结构复杂或需要关系查询,仍要用 Room | | Preferences DataStore 和 SharedPreferences 读写性能一样 | DataStore 底层是 Protobuf 编码,比 XML 更紧凑;且每次修改只更新增量字段,性能更优 | | Proto DataStore 只能存储简单对象 | 支持嵌套 message、枚举、oneof、map,可以存储较复杂的结构化数据 | | DataStore 的 Flow 会自动降级为普通回调 | Flow 的设计就是响应式,读取必须通过 `collect` 订阅;不能直接用 `getValue()` 阻塞获取 | ### 3.6 练一练(巩固理解) **选择题**(单选) 以下关于 DataStore 设计初衷的描述,哪一项是错误的? A. 替代 SharedPreferences 作为 Android 官方首选的键值对存储方案 B. 仅支持以键值对形式存储,不支持结构化对象 C. 使用 Kotlin Flow 提供响应式数据读取 D. 底层序列化基于 Protobuf 协议可以提高存储效率 > **答案:B** > 解释:DataStore 提供两种实现,其中 Proto DataStore 支持通过 .proto 文件定义结构化对象,并非“仅支持键值对”。 **判断题** - 在 Proto DataStore 中,如果修改了一个字段,整个文件会重新序列化写入磁盘。 ( ✗ ) 正确:DataStore 只序列化发生变化的字段,不是整个文件重写。 ### 3.7 应用边界:何时选择哪种? - **小规模配置项**(用户偏好、开关设置、登录 )→ 使用 **Preferences DataStore**,不需要定义 .proto 文件,开发成本最低。 - **结构化配置数据**(App 主题色 + 字体大小 + 语言多字段组合)→ 使用 **Proto DataStore**,强制字段类型和关系,避免代码中散落的硬编码键。 - **跨进程或模块共享配置** → 虽然 DataStore 底层依赖文件锁,但不推荐做进程间通信;此时应考虑 ContentProvider 或 Room 的 WAL 模式。 - **高频读写的复杂关系数据**(如用户的完整个人信息、商品列表)→ 仍应使用 **Room**,DataStore 不具备索引、关联查询、分页等能力。 > **本课总结**:DataStore 的出现不是为了替代 Room,而是专门替代 SharedPreferences 这个“历史遗留包袱”。它用 Protobuf 实现了类型安全,用 Flow 实现了响应式,但它仍然是**键值类存储**,不是关系型数据库。理解这个定位,才能做出正确的技术选型。