1. 简介
SharedPreferences 是 Android 平台提供的一种轻量级本地持久化机制,主要用于保存键值对(Key-Value)格式的数据,如用户设置、登录状态、缓存标识等。它的底层表现形式是一个 XML 文件,默认存储路径为:/data/data/<应用包名>/shared_prefs/<文件名>.xml。 由于其简便高效、易于使用的特点,在需要保存少量数据时非常常用。它的功能类似于 Windows 的 INI 文件或 iOS 中的 UserDefaults。
同时要注意的是,SharedPreferences 适用于存储体积小、结构简单的数据,不适合用于存储大型数据或高频写入的场景。
2. 操作模式
在创建其对象调用 getSharedPreferences(String name, int mode) 方法时,可以指定文件的访问模式,常见有以下几种:
Context.MODE_PRIVATE
- 默认模式。
- 文件只能被当前应用访问。
- 重复写入会覆盖旧内容(非追加)。
- 推荐使用,适用于大多数应用场景。
Context.MODE_APPEND(已废弃)
- 如果文件存在,将追加内容;否则创建新文件。
- 已废弃,并不适用于 SharedPreferences,更常用于 FileOutputStream。
- 不推荐使用。
Context.MODE_WORLD_READABLE 和 Context.MODE_WORLD_WRITEABLE(已废弃)
- 分别表示该文件可被其他应用读取或写入。
- 自 Android 4.2 开始已被废弃,出于安全性考虑,Google 官方强烈不推荐使用。
- 可以使用 ContentProvider 实现跨进程通信替代这些模式。
Context.MODE_MULTI_PROCESS(已废弃)
- 允许多个进程访问同一个 SharedPreferences 文件。
- 实现依赖进程间内存同步,但容易出问题(如缓存不同步)。
- 在 Android 6.0 以后基本无效,Google 推荐使用 ContentProvider 或 DataStore 替代。
3. 如何使用 SharedPreferences
3.1 获取 SharedPreferences 实例
// 获取默认的 SharedPreferences(基于包名+_preferences)
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
// 获取自定义名称的 SharedPreferences
val prefs = context.getSharedPreferences("my_prefs", Context.MODE_PRIVATE)
3.2 读取数据
val isFirstLaunch = prefs.getBoolean("first_launch", true)
val username = prefs.getString("username", null)
val age = prefs.getInt("age", 0)
3.3 写入数据
prefs.edit()
.putBoolean("first_launch", false)
.putString("username", "Mahu")
.putInt("age", 39)
.apply() // or .commit()
apply 和 commit 区别:
方法 |
是否异步 |
是否有返回值 |
应用场景 |
apply() |
✅ 是 |
❌ 无 |
推荐使用,效率高,不阻塞主线程 |
commit() |
❌ 否 |
✅ 有(返回成功/失败) |
用于需要立即确认是否写入成功的场景 |
3.4 监听数据变化
val listener = SharedPreferences.OnSharedPreferenceChangeListener { prefs, key ->
Log.d("PrefsChanged", "Key $key changed")
}
// 注册监听
prefs.registerOnSharedPreferenceChangeListener(listener)
// 使用完后解除注册
prefs.unregisterOnSharedPreferenceChangeListener(listener)
3.5 其他常用方法
// 是否存在某个键
prefs.contains("username")
// 移除某个键
prefs.edit().remove("username").apply()
// 清空所有数据
prefs.edit().clear().apply()
4. 源码简析
在 Android 系统中,SharedPreferences 的默认实现类是 SharedPreferencesImpl(源码路径是: frameworks/base/core/java/android/app/SharedPreferencesImpl.java)。它的核心原理是:
- 使用 内存中的 HashMap <String, Object>来缓存数据
- 使用文件系统中的 XML 文件持久化存储
- 通过 apply() 和 commit() 实现数据写入
- 使用锁机制确保 线程安全
4.1 数据结构:HashMap 内存缓存
SharedPreferencesImpl
维护一个成员变量 mMap
用于缓存当前内存中的数据内容:
@GuardedBy("mLock")
private Map<String, Object> mMap; // 实际类型是 HashMap
- 每次调用 getXXX() 方法时,都会从 mMap 中读取对应的值。
- 初次加载 XML 文件时,SharedPreferencesImpl 会通过 loadFromDisk() 方法将文件内容解析成 Map。
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
sLoadExecutor.execute(() -> {
loadFromDisk();
});
}
private void loadFromDisk() {
// ……
Map<String, Object> map = null;
// ……
map = (Map<String, Object>) XmlUtils.readMapXml(str);
// ……
synchronized (mLock) {
// ……
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
// ……
}
}
4.2 写操作流程:Editor 内部机制
当调用 edit() 方法时,会返回一个 EditorImpl 对象,它会记录修改意图,真正的写入操作由 apply() 或 commit() 决定。
@Override
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
4.3 apply() 的执行流程(异步写入)
@Override
public void apply() {
// 获取 mMap 的锁、将变更应用到 mMap 中、获取变更差异结果
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) { }
// …
}
};
// …
// 异步写入
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// 通知监听器
notifyListeners(mcr);
}
apply() 的特点:
- 使用线程池异步写入磁盘(不阻塞调用线程)
- 依然保证写入前对 mMap 的同步处理(线程安全)
- 不等待写入完成便直接通知监听器
4.4 commit() 的执行流程(同步写入)
@Override
public boolean commit() {
// 获取 mMap 的锁、将变更应用到 mMap 中、获取变更差异结果
MemoryCommitResult mcr = commitToMemory();
// 将 mMap 写入 XML 文件、写入完成后释放锁
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);
try {
// 等待写入完成
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
// 通知监听器
notifyListeners(mcr);
// 返回写入结果
return mcr.writeToDiskResult;
}
commit() 的特点:
- 同步写入磁盘,会阻塞调用线程
- 必须明确写入完成后再将结果通知监听器
4.5 锁机制与线程安全保障
为了避免多线程读写冲突,SharedPreferencesImpl
使用两个关键锁:
锁对象 |
作用说明 |
mLock |
用于保护 mMap 的初始化、读取和同步写入等 |
mWritingToDiskLock |
用于控制磁盘写操作,防止多线程同时写文件 |
此外,MemoryCommitResult
对象通过 CountDownLatch
实现 commit()
的同步等待效果。
5. 总结
目前 Google 官方已经推荐使用 Jetpack DataStore 替代 SharedPreferences,以解决其线程安全性差、缺乏类型安全等问题。 但是 SharedPreferences 也并非一无是处,它的显著的优点如:
- API 简单易懂、上手快:写法简单、门槛低,适合初学者或快速开发需求;
- 兼容性好:Android 1.0 起就内置支持,兼容所有版本;
- 性能满足小数据场景:对于配置项、布尔值、小型缓存等,小巧高效;
- 不依赖额外依赖库:无需额外引入 Jetpack 或 ProtoBuf,项目体积更小。
因此,在以下场景中,SharedPreferences 依然是合适的选择:
- 项目对数据同步和线程安全要求不高;
- 仅需保存简单的配置项或布尔标志位;
- 项目体积敏感、依赖较少。