## 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 自动刷新”。
下面这张对比图直观展示了两者的架构差异:
### 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 实现了响应式,但它仍然是**键值类存储**,不是关系型数据库。理解这个定位,才能做出正确的技术选型。