一、SharedPreferences是什么,作用是什么
SharedPreferences(简称 SP)是 Android 提供的轻量级键值对(Key-Value)存储方案,数据最终以 XML 文件形式持久化在:
/data/data/<包名>/shared_prefs/<文件名>.xml
典型用途:保存用户设置、登录 token、首次启动标记、简单配置等小体量、结构简单 的数据。不适合存大量数据或复杂结构。
文件内容:
xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="username">songyan</string>
<int name="launch_count" value="17" />
<boolean name="is_first_launch" value="false" />
</map>
二、基本使用
获取实例(三种方式)
java
// 1. 通过 Context,文件名自定义,最常用
SharedPreferences sp = context.getSharedPreferences("config", Context.MODE_PRIVATE);
// 2. Activity 专属,文件名默认就是 Activity 的类名
SharedPreferences sp2 = getPreferences(Context.MODE_PRIVATE);
// 3. 默认文件(包名_preferences),配合 Settings 页面用
SharedPreferences sp3 = PreferenceManager.getDefaultSharedPreferences(context);
MODE_PRIVATE 是唯一能用的模式,MODE_WORLD_READABLE、MODE_WORLD_WRITEABLE 在Android N 已废弃并抛异常(直接暴露数据给其他 App,安全性太差)。
写入
java
sp.edit()
.putString("username", "songyan")
.putInt("launch_count", 17)
.apply(); // 或 commit()
读取
java
String name = sp.getString("username", "默认值");
int count = sp.getInt("launch_count", 0);
三、apply() 和 commit() 的区别(高频面试点)
| commit() | apply() | |
|---|---|---|
| 写磁盘 | 同步,在调用线程直接写 | 异步,交给后台队列写 |
| 返回值 | 有,boolean 表示成功与否 |
无 |
| 阻塞主线程 | 会(有 ANR 风险) | 写入本身不会 |
| 失败通知 | 可感知 | 无感知 |
但注意:apply() 并不是完全没有 ANR 风险, 原因看下面的原理部分。
四、内部原理(源码视角)
1. 进程内单例缓存
ContextImpl.getSharedPreferences() 内部维护了一个静态 ArrayMap:
ArrayMap<String(包名), ArrayMap<File, SharedPreferencesImpl>>
同一个文件在整个进程内只有一个 SharedPreferencesImpl 实例,第一次创建后常驻内存。
2. 首次加载:异步读文件 + 全量进内存
创建 SharedPreferencesImpl 时会开子线程执行 loadFromFile(),把 XML 全量解析 进一个 HashMap 。之后所有 getXxx() 都直接读这个 HashMap------读操作没有磁盘 IO ,这就是 SP 快的原因。
代价是:
- 文件越大,首次加载越慢、内存占用越高 → SP 不要存大数据;
- 如果读操作发生在文件加载完成前,getXxx() 会在 awaitLoadedLocked() 里阻塞等待 ------所以不要在主线程第一次访问一个很大的 SP 文件。
3. apply() 的写流程
apply()
├── 内存:同步写入 HashMap,立即生效
└── 磁盘:构造一个写任务,丢进 QueuedWork 队列异步执行
关键点来了:系统在 ActivityThread.handleStopActivity()、handlePauseActivity()、handleServiceArgs() 等时机,会调用 QueuedWork.waitToFinish() 等待所有 pending 的写任务完成 。
也就是说 :如果你在主线程频繁 apply(),磁盘写入任务堆积,Activity 跳转/停止、Service 处理时主线程会被卡住等 IO------这是线上 ANR 的经典来源之一。这也是为什么很多团队埋点统计 "waitToFinish" 耗时。
4. commit() 的写流程
内存同步写入后,直接在调用线程执行 writeToFile(),写完返回结果。
5. 写文件的原子性
写磁盘时会先写到 .bak 备份逻辑(老版本)/ 先写临时文件再 rename 的机制保证:写一半崩溃了,还有备份兜底,不会得到一个残缺的 XML。
五、监听器的一个坑
java
SharedPreferences.OnSharedPreferenceChangeListener listener = (sp, key) -> {
Log.d("SP", key + " 变了");
};
sp.registerOnSharedPreferenceChangeListener(listener);
SP 内部用 WeakHashMap 存监听器------如果你注册的是一个没有强引用持有的匿名内部类/局部变量 ,GC 之后回调就再也不会触发了。必须把 listener 存成成员变量,不再用时 unregisterOnSharedPreferenceChangeListener()。
六、多进程问题
MODE_MULTI_PROCESS 早已被废弃且本身就不可靠 :SP 的内存缓存只在各自进程内生效,A 进程改了,B 进程的内存 HashMap 不会同步更新,数据会错乱。
跨进程共享数据的正确姿势:
- ContentProvider
- MMKV(腾讯开源,支持多进程)
- DataStore + 多进程方案(有限支持)
七、SP 的缺陷总结
- 全量加载进内存,大文件首读卡顿、占内存;
- apply() 的异步写可能引发主线程 ANR;
- 不支持多进程;
- 明文 XML 存储,敏感数据(token、密码)直接放不安全(需要加密或用 Keystore);
- 无类型安全,key 写错、类型存错都是运行时才炸。
八、现代替代方案
Jetpack DataStore(官方钦定的接班人):
- 基于 Kotlin 协程 + Flow,所有 IO 默认在后台线程,从机制上杜绝 ANR;
- 事务性更新,保证数据一致性;
- 两种形态:Preferences DataStore(键值对,对标 SP)和 Proto DataStore(protobuf,类型安全)
kotlin
// Preferences DataStore 示例
val Context.dataStore by preferencesDataStore(name = "settings")
val KEY_COUNT = intPreferencesKey("launch_count")
// 写
context.dataStore.edit { it[KEY_COUNT] = 17 }
// 读
context.dataStore.data.map { it[KEY_COUNT] ?: 0 }.collect { count -> ... }
MMKV(腾讯):基于 mmap 内存映射,性能极高,支持多进程,大厂 App 用得很多。
九、日常注意事项
- 只存小数据,单文件建议别超过几十 KB,配置多的话按业务拆多个文件;
- 写入统一用 apply(),但避免在主线程高频连续写(比如列表滚动时实时存进度);
- 首次读写提前到后台线程或启动阶段做,别卡在主线程关键路径;
- 敏感信息别明文存;
- 新项目直接上 DataStore,老项目渐进迁移(官方提供 SharedPreferencesMigration)。