前言
在很多 Android 项目中,配置存储一直是这样写的:
简单、直接,几乎人人都在用。
但在 Jetpack 体系中,Google 明确建议:
新项目应优先使用 DataStore,而不是 SharedPreferences。
于是网上出现了大量"DataStore 好在哪里"的文章,答案千篇一律:
- ✅ 支持 Flow
- ✅ 支持协程
- ✅ 类型更安全
这些当然都对。但如果只停留在这一层,你其实还没真正理解 Google 为什么要重新设计本地存储。
从并发架构角度看,本质区别只有一句话:
SharedPreferences 是「锁模型」,DataStore 是「无锁消息模型」。
一、SharedPreferences 的并发模型:锁
SharedPreferences 的核心实现类是 SharedPreferencesImpl,内部有两把关键锁:
分别负责:
- 内存 Map 的访问控制
- 磁盘写入的互斥保护
读取时:
写入时:
结论很清晰:
SharedPreferences 的并发安全,完全依赖锁来保证。
线程模型如下:
线程A ──┐
线程B ──┤──▶ 竞争锁 ──▶ 访问 SharedPreferences
线程C ──┘
二、锁 + 磁盘 IO = 定时炸弹
锁本身不是问题,真正的问题是:
锁 + 磁盘 IO 同时出现。
SharedPreferences 的数据以 XML 文件存储:
/data/data/<package>/shared_prefs/*.xml
当线程 A 正在写 XML,线程 B(主线程)想读取时,它必须等待 mLock 释放。
主线程一旦被阻塞:
UI 卡顿 → 极端情况下触发 ANR
三、apply() 也救不了你
很多人以为用 apply() 代替 commit() 就万事大吉了。
其实并没有。
apply() 的写磁盘操作确实是异步的,但 Android 系统在以下节点会强制等待所有 pending 任务完成:
commit() 的 ANR 发生在调用处,直接、明显;
apply() 的 ANR 发生在 onStop(),隐蔽,难排查。
commit() | apply() | |
|---|---|---|
| 阻塞时机 | 调用处 | onStop() |
| ANR 风险 | 明显 | 隐蔽 |
这也是 DataStore 用协程彻底解决这个问题的核心动机之一。
四、为什么 Google 不继续优化 SharedPreferences?
理论上可以改进锁策略:ReadWriteLock、CAS、更细粒度的锁……
但有一个根本问题无法绕过:
SharedPreferences 使用 XML 文件,而 XML 必须整体读取、整体写入。
这意味着锁的粒度几乎无法优化。局部修改不可能,并发写入不安全,架构层面已经走到了天花板。
五、DataStore:换一种思路——Actor 模型
Jetpack DataStore 采用了完全不同的并发模型:
线程A ──┐
线程B ──┤──▶ 发送消息 ──▶ Channel ──▶ 单协程顺序处理 ──▶ 写入磁盘
线程C ──┘
这就是 Actor 模型,核心思想是:
不共享状态,只通过消息通信。
updateData {} 本质上就是往 Channel 里发一条消息,由一个单独的协程负责顺序处理所有读写操作。
因为只有一个执行者,根本不需要锁。
六、Actor 模型带来的连锁优势
新的并发基础之上,DataStore 自然获得了一系列能力:
① 协程 IO,不阻塞主线程
所有磁盘操作都是 suspend 函数,彻底告别主线程 IO。
② 原子事务,不会部分写入
③ Flow 响应式,天然融入现代架构
无缝对接 Compose、MVI、响应式架构。
七、一张表说清楚
| SharedPreferences | DataStore | |
|---|---|---|
| 并发模型 | 锁模型 | Actor 消息模型 |
| IO 方式 | 同步 / 伪异步 | 纯异步(协程) |
| 原子性 | ❌ | ✅ |
| ANR 风险 | 有,且隐蔽 | 无 |
| 响应式支持 | ❌ | ✅ Flow |
| 类型安全 | ❌ | ✅(Proto) |
总结
SharedPreferences 诞生于 Android 早期——那时多线程简单,数据规模小,XML 够用。
但今天,协程、Flow、Compose、响应式架构已经是主流。在这个背景下,「锁模型」逐渐被「消息模型」取代,是架构演进的必然结果。
表面是 API 的一次升级,本质是并发模型的彻底重构——读懂这一点,才算真正理解了 DataStore。