本文内容来自:却把青梅嗅:反思|官方也无力回天?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 {@Overridepublic 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。
SP 导致 ANR

