第2章:SharedPreferences 原理解析

2.1 概念与核心角色

SharedPreferences 是 Android 官方提供的一种轻量级键值对持久化方案,专用于保存 简单配置 / 用户偏好 数据。其本质是一个运行在 单进程 内的、基于 XML 文件 的、默认 线程安全 的数据仓库。开发者通过 getSharedPreferences(name, mode) 获取实例,之后像操作 Map 一样读写数据,最后通过 commit() 或 apply() 将改动写回文件。

一句话类比: SharedPreferences 就像酒店每个房间的“床头意见簿”——你可以随时写下几行简短笔记(键值对),酒店后台会将多本意见簿统一归档为一个 XML 文件;但意见簿永远只属于当前房间(进程),不能跨楼层共享。

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>
SharedPreferences 支持的数据类型与 XML 标签
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() 才会写回文件。

性能关键点: 因为采用全量加载,单文件不宜过大(推荐不超过 100 个键值对)。如果 XML 文件过大,首次访问会阻塞调用线程,造成界面卡顿。
SharedPreferences 读写流程示意图 进程内部(单进程) App 代码 SharedPreferences 实例 内存 HashMap Editor 内部缓存 XML 磁盘文件 流程说明: ① getSharedPreferences() 时全量加载 XML 至内存 HashMap(虚线双向箭头) ② 读取操作直接查询内存,无需 I/O ③ 写入操作先暂存 Editor 缓存,最后通过 commit/apply 写回 XML 文件

2.4 线程安全机制

SharedPreferences 实例自带 内部锁机制:所有 getXXX() 和 putXXX() 操作都通过 synchronized 块保护,因此 多线程并发读取是安全的。但需要注意以下两个细节:

  1. commit() 是同步写盘:当前线程会阻塞直到写入完成;
  2. apply() 是异步写盘:将写入任务提交到 单一后台线程(由 QueuedWork 管理),不阻塞调用线程。
commit() vs apply() 对比
特性commit()apply()
写盘方式同步(当前线程阻塞)异步(后台单线程)
返回值boolean(成功/失败)void
推荐场景需要立即知道写入结果(如设置向导最后一步)绝大多数写入,避免阻塞 UI 线程
异常风险I/O 异常直接抛出异常仅打印日志,不反馈调用层
常见误区 1: 认为 apply() 安全。事实上,如果连续快速调用 apply() 多次,只有最后一次的写入会真正落盘,中间状态可能丢失。若需要保证每次写入都生效(例如计数器递增),应使用 commit() 或结合 AtomicInteger 等自行同步。

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() 时重新读取文件,而多进程同时写入时仍会覆盖。
正确做法: 跨进程数据共享请使用 ContentProvider、文件锁 + MMKV 或直接使用 Binder + 数据库(Room)。SharedPreferences 绝不能用于多进程场景。

2.6 常见误区与避坑指南

  • 误区 2:getXXX() 传入默认值会写入文件? 不会。只有显式调用 putXXX() 并 commit/apply 后数据才会持久化。默认值只在内存中返回。
  • 误区 3:edit() 之后不调用 commit/apply 数据就丢了? 是的。所有的改动都缓存在 Editor 内部,不写回文件则重启后消失。
  • 误区 4:同一个 name 的 SharedPreferences 可以在多进程间安全传递? 不可以,如前所述。
  • 误区 5:大量数据也放 SharedPreferences? 错误。XML 全量解析拖慢启动,建议改用数据库或文件存储。

2.7 练习题(自我检测)

  1. SharedPreferences 的 XML 文件中,<int> 标签的 value 属性是否需要加引号?
  2. 如果连续调用 putString("key1", "a"); apply(); putString("key1", "b"); apply();,在极短时间内执行,最终 “key1” 的值是什么?
  3. 为什么不能使用 SharedPreferences 在两个独立 App 之间共享数据?
  4. 下列哪种情况适合使用 SharedPreferences?
    A. 保存用户登录后的购物车列表(50 件商品)
    B. 保存用户设置的字体大小(int)
    C. 保存应用的全局唯一激活码(String)
    D. 保存 App 自身的依赖库缓存路径

2.8 应用边界总结

SharedPreferences 适用边界
场景推荐使用?替代方案
用户偏好设置(主题、字体、开关)✅ 是—
少量应用配置(首次启动标记、版本号)✅ 是—
跨进程共享配置❌ 否ContentProvider / MMKV
复杂结构化数据(列表、嵌套对象)❌ 否Room / DataStore
高频写入且要求强持久化(如日志计数器)⚠️ 谨慎(使用 commit 或自建同步)DataStore / 文件
存储敏感信息(密码、)❌ 否EncryptedSharedPreferences(加密封装)
结论: SharedPreferences 是 Android 持久化家族的“轻骑兵”——适合简单、低频、单进程、少量键值对的场景。跨过边界使用它,就会暴露其线程安全假象与跨进程短板。理解其 XML 文件格式、内存缓存模型和 commit/apply 差异,是正确选型的基石。