本文内容来自:却把青梅嗅:反思|官方也无力回天?Android SharedPreferences的设计与实现

简介

SharedPreferences 作为轻量持久化存储的手段被广为使用,不过逐渐被腾讯开源的 MMKV 和官方新推出的 Jetpack DataStore 取代。

本质

以 键值对(key-value)的方式保存数据的 xml 文件,其保存在 /data/data/shared_prefs 目录下。

使用

  1. val sp = getSharedPreferences("data", Context.MODE_PRIVATE)
  2. val editor = sp.edit().apply {
  3. putString("name", "name")
  4. putInt("age", 1)
  5. putBoolean("marry", false)
  6. }
  7. editor.apply()

模型

SharedPreferencesImpl

SharedPreferences 对象第一次通过 Context.getSharedPreferences() 进行初始化时,对 xml 文件进行一次读取,并将文件内所有内容(即所有的键值对)缓到内存的一个 Map 中,这样,接下来所有的读操作,只需要从这个 Map 中取就可以了:

  1. final class SharedPreferencesImpl implements SharedPreferences {
  2. private final File mFile; // 对应的xml文件
  3. private Map<String, Object> mMap; // Map中缓存了xml文件中所有的键值对
  4. }

Editor

在复杂的业务中,有时候一次操作会导致多个键值对的更新,这时,与其多次更新文件,我们更倾向将这些更新 合并到一次写操作 中,以达到性能的优化。

因此,对于 SharedPreferences 的写操作,设计者抽象出了一个 Editor 类,不管某次操作通过若干次调用putXXX()方法,更新了几个 xml 中的键值对,只有调用了 commit() 方法,最终才会真正写入文件:

  1. // 简单的业务,一次更新一个键值对
  2. sharedPreferences.edit().putString().commit();
  3. // 复杂的业务,一次更新多个键值对,仍然只进行一次IO操作(文件的写入)
  4. Editor editor = sharedPreferences.edit();
  5. editor.putString();
  6. editor.putBoolean().putInt();
  7. editor.commit(); // commit()才会更新文件

文件的 I/O 是一个非常重的操作,直接放在主线程中的 commit() 方法某些场景下会导致 ANR(比如数据量过大),因此更合理的方式是应该将其放入子线程执行。
因此设计者还为 Editor 提供了一个 apply() 方法,用于异步执行文件数据的同步,并推荐开发者使用apply() 而非 commit()

每次在调用 editor.putXXX() 时,实际上会将新的数据存入在 mMap,当调用 commit()apply() 时,最终会将 mMap 的所有数据全量更新到 xml 文件里。

保证线程安全

为了保证 SharedPreferences 是线程安全的,Google的设计者一共使用了 3 把锁:

mLock,mEditorLock,mWritingToDiskLock

  1. final class SharedPreferencesImpl implements SharedPreferences {
  2. // 1、使用注释标记锁的顺序
  3. // Lock ordering rules:
  4. // - acquire SharedPreferencesImpl.mLock before EditorImpl.mLock
  5. // - acquire mWritingToDiskLock before EditorImpl.mLock
  6. // 2、通过注解标记持有的是哪把锁
  7. @GuardedBy("mLock")
  8. private Map<String, Object> mMap;
  9. @GuardedBy("mWritingToDiskLock")
  10. private long mDiskStateGeneration;
  11. public final class EditorImpl implements Editor {
  12. @GuardedBy("mEditorLock")
  13. private final Map<String, Object> mModified = new HashMap<>();
  14. }
  15. }

对于简单的 读操作 而言,原理是读取内存中mMap的值并返回,那么为了保证线程安全,只需要加一把锁保(mLock)证mMap 的线程安全即可:

  1. public String getString(String key, @Nullable String defValue) {
  2. synchronized (mLock) {
  3. String v = (String)mMap.get(key);
  4. return v != null ? v : defValue;
  5. }
  6. }

对于写操作而言,每次 putXXX() 并不能立即更新在 mMap 中,这是理所当然的,如果开发者没有调用 apply() 方法,那么这些数据的更新理所当然应该被抛弃掉,但是如果直接更新在 mMap 中,那么数据就难以恢复。
因此,Editor 本身也应该持有一个 mEditorMap 对象,用于存储数据的更新;只有当调用 apply() 时,才尝试将 mEditorMapmMap 进行合并,以达到数据更新的目的。因此需要另外一把锁(mEditorLock)保证 mEditorMap 的线程安全。

  1. public final class EditorImpl implements Editor {
  2. @Override
  3. public Editor putString(String key, String value) {
  4. synchronized (mEditorLock) {
  5. mEditorMap.put(key, value);
  6. return this;
  7. }
  8. }
  9. }

文件写入时也需要一把锁(mWritingToDiskLock

  1. // SharedPreferencesImpl.EditorImpl.enqueueDiskWrite()
  2. synchronized (mWritingToDiskLock) {
  3. writeToFile(mcr, isFromSyncCommit);
  4. }

进程安全

SharedPreferences 不支持多进程访问

常见问题

如何设计支持多进程的 SP?

思路很多,比如使用文件锁,保证每次只有一个进程访问该文件。也可以借助 ContentProvider 访问 SP。

SP 导致 ANR

image.png