一、SharedPreferences 的核心存储机制揭秘
SharedPreferences(简称 SP)作为 Android 中最常用的轻量级存储方案,其底层实现暗藏诸多设计细节。当我们调用getSharedPreferences
时,系统会按以下流程工作:
-
文件与缓存机制
- SP 以 XML 文件形式存储在
/data/data/包名/shared_prefs/
目录,文件名由用户指定(如gityuan.xml
)。 ContextImpl
通过静态缓存sSharedPrefsCache
维护所有 SP 实例,以包名→文件→SharedPreferencesImpl
的三层映射结构避免重复创建实例,提升性能。
- SP 以 XML 文件形式存储在
-
加载流程的线程设计
-
首次获取 SP 时,
SharedPreferencesImpl
会创建新线程异步加载 XML 文件(loadFromDisk()
)。 -
加载完成前,所有
getXXX()
和edit()
操作会被阻塞(通过wait()
等待线程通知),直到数据完全载入内存的mMap
中。
-
关键源码片段:
java
// SharedPreferencesImpl初始化时启动异步加载
SharedPreferencesImpl(File file, int mode) {
startLoadFromDisk(); // 开启新线程加载文件
}
private void loadFromDisk() {
// 解析XML文件并将数据存入mMap
map = XmlUtils.readMapXml(str);
synchronized (this) {
mLoaded = true;
notifyAll(); // 唤醒阻塞的线程
}
}
二、Editor 操作的核心流程与坑点
当调用edit()
获取编辑器时,实际上创建的是EditorImpl
实例,其数据操作分为两步:
-
内存暂存阶段
putXXX()
操作仅将数据存入EditorImpl.mModified
map,此时未影响磁盘文件。remove()
操作通过存入特殊值this
标记删除,clear()
则标记mClear=true
。
-
提交阶段:commit 与 apply 的本质差异
-
commit() :同步写入磁盘,返回布尔值表示是否成功。调用时会阻塞线程直到写入完成,适合需确认结果的场景(如登录状态保存)。
-
apply() :异步写入磁盘,无返回值。数据先提交到内存
mMap
,再通过单线程池写入文件,适合频繁提交的场景(如配置项更新)。
-
性能对比案例:
在 1000 次连续提交测试中:
-
commit () 耗时约 150ms(主线程阻塞),
-
apply () 耗时约 30ms(异步执行)。
核心差异源码:
java
// commit()同步等待磁盘写入
public boolean commit() {
mcr.writtenToDiskLatch.await(); // 阻塞等待
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
// apply()异步提交
public void apply() {
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable); // 线程池执行
notifyListeners(mcr);
}
三、多进程与性能优化的实战指南
-
多进程陷阱与替代方案
-
MODE_MULTI_PROCESS
模式通过每次加载时对比文件时间戳实现跨进程同步,但存在以下问题:- 频繁加载导致性能损耗;
- 无法处理并发写入冲突(后写入的进程会覆盖先写入的数据)。
-
推荐方案:使用
ContentProvider
或DataStore
配合ViewModel
实现跨进程数据共享。
-
-
高频操作优化策略
-
批量提交:避免多次调用
edit()
,应合并操作后一次性提交:java
SharedPreferences.Editor editor = sp.edit(); editor.putString("key1", "value1"); editor.putInt("key2", 2); editor.apply(); // 一次提交所有修改
-
拆分文件:将高频读写的 key 与低频操作的 key 分离到不同 SP 文件,减少锁竞争:
java
// 高频配置单独存储 SharedPreferences frequentSp = getSharedPreferences("frequent_config", MODE_PRIVATE); // 低频数据存储 SharedPreferences infrequentSp = getSharedPreferences("infrequent_data", MODE_PRIVATE);
-
-
ANR 预防与监控
-
SP 导致 ANR 的常见场景:
- 在主线程执行大量 commit () 操作;
- 加载大文件时阻塞主线程。
-
监控方案:通过
StrictMode
检测主线程磁盘操作:java
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectDiskReads() .detectDiskWrites() .penaltyLog() .build());
-
四、与 DataStore 的对比与迁移建议
Android Jetpack 推出的DataStore
作为 SP 的替代方案,在以下场景更具优势:
特性 | SharedPreferences | DataStore(Preferences) |
---|---|---|
数据类型 | 基本类型 + StringSet | 基本类型 + Flow 响应式数据 |
线程安全 | 需手动处理 | 自动异步处理 |
性能(大数据量) | 较差(全量写入) | 更好(增量更新) |
跨进程支持 | 有限(MODE_MULTI_PROCESS) | 内置支持 |
迁移示例:
java
// 旧方案:SP同步提交
SharedPreferences sp = getSharedPreferences("config", MODE_PRIVATE);
sp.edit().putInt("theme", 1).commit();
// 新方案:DataStore异步提交
val dataStore = context.createDataStore("config")
dataStore.edit { preferences ->
preferences["theme"] = "1"
}
五、常见问题与源码级解决方案
-
数据丢失问题
- 原因:写入磁盘时进程被杀,未完成的事务导致数据未更新。
- 解决方案:SP 通过
.bak
备份文件实现部分恢复(写入前重命名原文件为 bak,成功后删除)。
-
内存泄漏风险
- 原因:长时间持有
SharedPreferencesImpl
实例,或未移除OnSharedPreferenceChangeListener
。 - 解决方案:在 Activity/Fragment 销毁时调用
unregisterOnSharedPreferenceChangeListener
。
- 原因:长时间持有
-
跨版本兼容性
- Android N 后禁止
MODE_WORLD_READABLE
和MODE_WORLD_WRITEABLE
,需改用权限控制。 - Android 10 + 中 SP 文件默认存储在沙盒目录,外部应用无法直接访问。
- Android N 后禁止
六、总结:SP 的正确打开方式
-
适用场景:
- 轻量级配置(如主题、语言、登录状态);
- 少量高频读写的数据(如计数器)。
-
禁忌场景:
- 存储大文本(如 JSON 字符串);
- 频繁在主线程执行 commit ();
- 跨进程共享数据(推荐 DataStore)。
-
最佳实践口诀:
-
小数据、轻量级,apply 异步更合适;
-
批量提交少调用,文件拆分减竞争;
-
多进程用新方案,内存泄漏及时清。
-
通过深入理解 SP 的源码机制与性能瓶颈,开发者可在实际项目中规避陷阱,结合 DataStore 等新方案实现更高效的数据存储。