第2章:SharedPreferences 原理解析
2.1 概念与核心角色
SharedPreferences 是 Android 官方提供的一种轻量级键值对持久化方案,专用于保存 简单配置 / 用户偏好 数据。其本质是一个运行在 单进程 内的、基于 XML 文件 的、默认 线程安全 的数据仓库。开发者通过 getSharedPreferences(name, mode) 获取实例,之后像操作 Map 一样读写数据,最后通过 commit() 或 apply() 将改动写回文件。
2.2 文件格式:纯 XML 结构
每个 SharedPreferences 实例对应一个 XML 文件,路径为 /data/data/<package>/shared_prefs/<name>.xml。其根节点始终是 <map>,内部按写入顺序平铺 <string>、<int>、<boolean>、<float>、<long> 等类型元素。以下是一个典型文件内容:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="username">alice</string>
<int name="login_count" value="12" />
<boolean name="dark_mode" value="true" />
<float name="font_size" value="16.0" />
</map>
| Java 类型 | XML 标签 | 值属性 | 示例 |
|---|---|---|---|
| String | <string> | 文本内容 | <string name="key">value</string> |
| int | <int> | value | <int name="key" value="42" /> |
| boolean | <boolean> | value | <boolean name="key" value="true" /> |
| float | <float> | value | <float name="key" value="3.14" /> |
| long | <long> | value | <long name="key" value="100000L" /> |
2.3 读写机制与内存缓存
SharedPreferences 采用 “全量加载、增量提交” 策略。首次调用 getSharedPreferences() 时,会通过 XmlUtils.readMapXml() 将整个 XML 文件解析为一个 HashMap<String, Object> 并常驻内存。后续的所有 getXXX() 操作均直接访问内存,不涉及文件 I/O;只有 putXXX() 时数据暂存在一个 Editor 内部缓存 中,直到调用 commit() 或 apply() 才会写回文件。
2.4 线程安全机制
SharedPreferences 实例自带 内部锁机制:所有 getXXX() 和 putXXX() 操作都通过 synchronized 块保护,因此 多线程并发读取是安全的。但需要注意以下两个细节:
- commit() 是同步写盘:当前线程会阻塞直到写入完成;
- apply() 是异步写盘:将写入任务提交到 单一后台线程(由
QueuedWork管理),不阻塞调用线程。
| 特性 | commit() | apply() |
|---|---|---|
| 写盘方式 | 同步(当前线程阻塞) | 异步(后台单线程) |
| 返回值 | boolean(成功/失败) | void |
| 推荐场景 | 需要立即知道写入结果(如设置向导最后一步) | 绝大多数写入,避免阻塞 UI 线程 |
| 异常风险 | I/O 异常直接抛出 | 异常仅打印日志,不反馈调用层 |
2.5 跨进程限制与原因
SharedPreferences 默认工作模式为 MODE_PRIVATE,文件仅被当前进程访问。Android 早期提供了 MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE,但自 API 17 起已废弃。即使用 Context.MODE_MULTI_PROCESS 标记(API 23 起实际上已无效),SharedPreferences 也 无法实现可靠的跨进程数据共享,原因如下:
- 内存缓存不一致: 每个进程持有独立的内存 HashMap,A 进程写入后 B 进程的缓存不会自动刷新;
- 文件锁缺失: 写回时没有使用跨进程文件锁,高并发下可能导致数据损坏;
- 原子性无保证: 即使使用
Mode.MULTI_PROCESS,也仅是在每次 getSharedPreferences() 时重新读取文件,而多进程同时写入时仍会覆盖。
2.6 常见误区与避坑指南
- 误区 2:getXXX() 传入默认值会写入文件? 不会。只有显式调用
putXXX()并commit/apply后数据才会持久化。默认值只在内存中返回。 - 误区 3:edit() 之后不调用 commit/apply 数据就丢了? 是的。所有的改动都缓存在 Editor 内部,不写回文件则重启后消失。
- 误区 4:同一个 name 的 SharedPreferences 可以在多进程间安全传递? 不可以,如前所述。
- 误区 5:大量数据也放 SharedPreferences? 错误。XML 全量解析拖慢启动,建议改用数据库或文件存储。
2.7 练习题(自我检测)
- SharedPreferences 的 XML 文件中,
<int>标签的 value 属性是否需要加引号? - 如果连续调用
putString("key1", "a"); apply(); putString("key1", "b"); apply();,在极短时间内执行,最终 “key1” 的值是什么? - 为什么不能使用 SharedPreferences 在两个独立 App 之间共享数据?
- 下列哪种情况适合使用 SharedPreferences?
A. 保存用户登录后的购物车列表(50 件商品)
B. 保存用户设置的字体大小(int)
C. 保存应用的全局唯一激活码(String)
D. 保存 App 自身的依赖库缓存路径
2.8 应用边界总结
| 场景 | 推荐使用? | 替代方案 |
|---|---|---|
| 用户偏好设置(主题、字体、开关) | ✅ 是 | — |
| 少量应用配置(首次启动标记、版本号) | ✅ 是 | — |
| 跨进程共享配置 | ❌ 否 | ContentProvider / MMKV |
| 复杂结构化数据(列表、嵌套对象) | ❌ 否 | Room / DataStore |
| 高频写入且要求强持久化(如日志计数器) | ⚠️ 谨慎(使用 commit 或自建同步) | DataStore / 文件 |
| 存储敏感信息(密码、) | ❌ 否 | EncryptedSharedPreferences(加密封装) |