本文内容来自:却把青梅嗅:反思|官方也无力回天?Android SharedPreferences的设计与实现
简介
SharedPreferences 作为轻量持久化存储的手段被广为使用,不过逐渐被腾讯开源的 MMKV 和官方新推出的 Jetpack DataStore 取代。
本质
以 键值对(key-value)的方式保存数据的 xml
文件,其保存在 /data/data/shared_prefs
目录下。
使用
val sp = getSharedPreferences("data", Context.MODE_PRIVATE)
val editor = sp.edit().apply {
putString("name", "name")
putInt("age", 1)
putBoolean("marry", false)
}
editor.apply()
模型
SharedPreferencesImpl
当 SharedPreferences
对象第一次通过 Context.getSharedPreferences()
进行初始化时,对 xml
文件进行一次读取,并将文件内所有内容(即所有的键值对)缓到内存的一个 Map
中,这样,接下来所有的读操作,只需要从这个 Map
中取就可以了:
final class SharedPreferencesImpl implements SharedPreferences {
private final File mFile; // 对应的xml文件
private Map<String, Object> mMap; // Map中缓存了xml文件中所有的键值对
}
Editor
在复杂的业务中,有时候一次操作会导致多个键值对的更新,这时,与其多次更新文件,我们更倾向将这些更新 合并到一次写操作 中,以达到性能的优化。
因此,对于 SharedPreferences
的写操作,设计者抽象出了一个 Editor
类,不管某次操作通过若干次调用putXXX()
方法,更新了几个 xml
中的键值对,只有调用了 commit()
方法,最终才会真正写入文件:
// 简单的业务,一次更新一个键值对
sharedPreferences.edit().putString().commit();
// 复杂的业务,一次更新多个键值对,仍然只进行一次IO操作(文件的写入)
Editor editor = sharedPreferences.edit();
editor.putString();
editor.putBoolean().putInt();
editor.commit(); // commit()才会更新文件
文件的 I/O
是一个非常重的操作,直接放在主线程中的 commit()
方法某些场景下会导致 ANR
(比如数据量过大),因此更合理的方式是应该将其放入子线程执行。
因此设计者还为 Editor
提供了一个 apply()
方法,用于异步执行文件数据的同步,并推荐开发者使用apply()
而非 commit()
。
每次在调用 editor.putXXX()
时,实际上会将新的数据存入在 mMap
,当调用 commit()
或 apply()
时,最终会将 mMap
的所有数据全量更新到 xml
文件里。
保证线程安全
为了保证 SharedPreferences
是线程安全的,Google
的设计者一共使用了 3 把锁:
mLock,mEditorLock,mWritingToDiskLock
final class SharedPreferencesImpl implements SharedPreferences {
// 1、使用注释标记锁的顺序
// Lock ordering rules:
// - acquire SharedPreferencesImpl.mLock before EditorImpl.mLock
// - acquire mWritingToDiskLock before EditorImpl.mLock
// 2、通过注解标记持有的是哪把锁
@GuardedBy("mLock")
private Map<String, Object> mMap;
@GuardedBy("mWritingToDiskLock")
private long mDiskStateGeneration;
public final class EditorImpl implements Editor {
@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();
}
}
对于简单的 读操作 而言,原理是读取内存中mMap
的值并返回,那么为了保证线程安全,只需要加一把锁保(mLock)证mMap
的线程安全即可:
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
对于写操作而言,每次 putXXX()
并不能立即更新在 mMap
中,这是理所当然的,如果开发者没有调用 apply()
方法,那么这些数据的更新理所当然应该被抛弃掉,但是如果直接更新在 mMap
中,那么数据就难以恢复。
因此,Editor
本身也应该持有一个 mEditorMap
对象,用于存储数据的更新;只有当调用 apply()
时,才尝试将 mEditorMap
与 mMap
进行合并,以达到数据更新的目的。因此需要另外一把锁(mEditorLock)保证 mEditorMap
的线程安全。
public final class EditorImpl implements Editor {
@Override
public Editor putString(String key, String value) {
synchronized (mEditorLock) {
mEditorMap.put(key, value);
return this;
}
}
}
文件写入时也需要一把锁(mWritingToDiskLock)
// SharedPreferencesImpl.EditorImpl.enqueueDiskWrite()
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
进程安全
SharedPreferences 不支持多进程访问
常见问题
如何设计支持多进程的 SP?
思路很多,比如使用文件锁,保证每次只有一个进程访问该文件。也可以借助 ContentProvider 访问 SP。