android SharedPreferences源码

169 阅读10分钟

SP

sp概述

Android SharedPreferences 用于存储用户本地持久化数据。其特点:

  1. 轻量级储存
  2. 以键值对(key-value)的方式保存数据的xml文件,其保存在/data/data/shared_prefs目录下

项目中已迁移使用MMKV或者DataStore,为什么还热衷于SP的源码呢?

有没有可能知道我不会MMKV或者DataStore源码呢,我想大概是的
TODO MMKV或者DataStore原理

读操作

当SharedPreferences对象第一次通过Context.getSharedPreferences()进行初始化时,对xml文件进行一次读取,并将文件内所有内容(即所有的键值对)缓到内存的一个Map中

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

备注:详细的过程,ANR部分再解析

写操作

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

commit 调用线程写操作

commit 源码

@Override
public boolean commit() {
 
      MemoryCommitResult mcr = commitToMemory();//内存保存
      SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* sync write on this thread okay */);//第二个参数为null

      return mcr.writeToDiskResult;
}
	  

可以看到enqueueDiskWrite的第二个参数为null

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                            final Runnable postWriteRunnable) {
      final boolean isFromSyncCommit = (postWriteRunnable == null);//此时postWriteRunnable为null,isFromSyncCommit 则为true

      final Runnable writeToDiskRunnable = new Runnable() {
              @Override
              public void run() {
                  synchronized (mWritingToDiskLock) {
                      writeToFile(mcr, isFromSyncCommit);
                  }
                  synchronized (mLock) {
                      mDiskWritesInFlight--;
                  }
                  if (postWriteRunnable != null) {
                      postWriteRunnable.run();
                  }
              }
          };

      // Typical #commit() path with fewer allocations, doing a write on
      // the current thread.
      if (isFromSyncCommit) {  //当调用commit方法时,isFromSyncCommit则为true
          boolean wasEmpty = false;
          synchronized (mLock) {
              wasEmpty = mDiskWritesInFlight == 1;
          }
          if (wasEmpty) {
              writeToDiskRunnable.run();//主线程回调writeToDiskRunnable的run方法,进行writeToFile文件的存储
              return;
          }
      }

      QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
		  

postWriteRunnable为null,则isFromSyncCommit为true,代码会在主线程回调writeToDiskRunnable的run方法,进行writeToFile文件的存储。这部分动作直接在主线程执行,如果文件较大,则主线程也会因为IO时间长造成ANR的。

apply 异步线程写操作

apply()方法设计的初衷是为了规避主线程的I/O操作导致ANR问题的产生,那么ANR的问题真得到了有效的解决吗?

apply 源码

public void apply() {
     ```
    final Runnable awaitCommit = new Runnable() {
    public void run() {
        try {
            mcr.writtenToDiskLatch.await();
        } catch (InterruptedException ignored) {
        }

    };
            // 将 awaitCommit 添加到队列 QueuedWork 中
    QueuedWork.addFinisher(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
    public void run() {
        awaitCommit.run();
        QueuedWork.removeFinisher(awaitCommit);// 将 awaitCommit 从队列 QueuedWork 中移除
    }
    };
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
     ```
}

apply方法最后调用了SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable)

enqueueDiskWrite方法会把存储文件的动作放到子线程,这个方法的第二个参数postWriteRunnable做了两件事:

  1. 让awaitCommit执行 及执行 mcr.writtenToDiskLatch.await();
  2. 执行QueuedWork.remove(awaitCommit);
CountDownLatch

writtenToDiskLatch的类型是CountDownLatch,CountDownLatch是一个同步工具类,通过一个计数器来实现的同步。当一个线程完成了自己的任务调用countDown(),则计数器的值就相应得减1。当计数器到达0时,表示所有的线程都已执行完毕,然后在等待的线程await()就可以恢复执行任务。

  1. countDown(): 对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。

  2. await(): 阻塞当前线程,将当前线程加入阻塞队列。

可以看到如果postWriteRunnable方法被触发执行的话,由于 mcr.writtenToDiskLatch.await()的缘故,UI线程会被一直阻塞住,等待计数器减至0才能被唤醒。

enqueueDiskWrite源码如下所示:

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                final Runnable postWriteRunnable) {
      final boolean isFromSyncCommit = (postWriteRunnable == null);
      final Runnable writeToDiskRunnable = new Runnable() {
              public void run() {
                  synchronized (mWritingToDiskLock) {
                      writeToFile(mcr, isFromSyncCommit);
                  }
                  synchronized (mLock) {
                      mDiskWritesInFlight--;
                  }
                  if (postWriteRunnable != null) {
                      postWriteRunnable.run();
                  }
              }
          };
      // Typical #commit() path with fewer allocations, doing a write on
      // the current thread.
      if (isFromSyncCommit) {
          boolean wasEmpty = false;
          synchronized (mLock) {
              wasEmpty = mDiskWritesInFlight == 1;
          }
          if (wasEmpty) {
              writeToDiskRunnable.run();
              return;
          }
      }
      QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
  }
		  

很明显postWriteRunnable不为null,程序会执行QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);

writeToDiskRunnable:

  1. writeToFile(): 内容存储到文件
  2. postWriteRunnable.run()

QueuedWork.queue源码:

public static void queue(Runnable work, boolean shouldDelay) {
      Handler handler = getHandler();
      synchronized (sLock) {
          sWork.add(work);
          if (shouldDelay && sCanDelay) {
              handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
          } else {
              handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
          }
      }
  }


private static class QueuedWorkHandler extends Handler {
    static final int MSG_RUN = 1;

     QueuedWorkHandler(Looper looper) {
        super(looper);
    }


    public void handleMessage(Message msg) {
        if (msg.what == MSG_RUN) {
            processPendingWork();
        }
    }
}

就是把writeToDiskRunnable这个任务放到sWork这个list中,并且执行handler,根据HandlerThread的知识点,我们知道handlermessage里面就是子线程了。 接下来我们继续看handleMessage里面的processPendingWork()方法:

private static void processPendingWork() {
      long startTime = 0;

      if (DEBUG) {
          startTime = System.currentTimeMillis();
      }

      synchronized (sProcessingWork) {
          LinkedList<Runnable> work;

          synchronized (sLock) {
              work = (LinkedList<Runnable>) sWork.clone();
              sWork.clear();

              // Remove all msg-s as all work will be processed now
              getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
          }

          if (work.size() > 0) {
              for (Runnable w : work) {
                  w.run();
              }

              if (DEBUG) {
                  Log.d(LOG_TAG, "processing " + work.size() + " items took " +
                          +(System.currentTimeMillis() - startTime) + " ms");
              }
          }
      }
  }

是把sWork克隆给work,然后开启循环,执行work对象的run方法,及调用writeToDiskRunnable的run方法。上面讲过了,他里面做了两件事:

  1. 内容存储到文件
  2. postWriteRunnable方法回调

执行run方法的代码:

final Runnable writeToDiskRunnable = new Runnable() {
    public void run() {
        synchronized (mWritingToDiskLock) {
            writeToFile(mcr, isFromSyncCommit);//由于handlermessage在子线程,则writeToFile也在子线程中
        }
        synchronized (mLock) {
            mDiskWritesInFlight--;
        }
        if (postWriteRunnable != null) {
            postWriteRunnable.run();
        }
    }
};
			  

writeToFile方法我们不深入去看,但是要关注,里面有个setDiskWriteResult方法,在该方法里面做了如下的事情:

void setDiskWriteResult(boolean wasWritten, boolean result) {
        this.wasWritten = wasWritten;
        writeToDiskResult = result;
        writtenToDiskLatch.countDown();//计数器-1
    }

当调用countDown()方法时,会对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。

sp-第 3 页.png

问题:子线程为什么要去执行postWriteRunnable呢?子线程为什么需要await? 也没有看到会出现ANR的情况

数据的更新

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

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
    // name参数就是文件名,通过不同文件名,获取指定的SharedPreferences对象
}

线程安全

  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的值并返回,那么为了保证线程安全,只需要加一把锁保证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进行合并,以达到数据更新的目的。

public final class EditorImpl implements Editor {
    @Override
    public Editor putString(String key, String value) {
        synchronized (mEditorLock) {
            mEditorMap.put(key, value);
            return this;
        }
}

文件更新:

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

文件损坏 & 备份机制

由于不可预知的原因(比如内核崩溃或者系统突然断电),xml文件的写操作异常中止,Android系统本身的文件系统虽然有很多保护措施,但依然会有数据丢失或者文件损坏的情况。

  1. 对文件进行备份,SharedPreferences的写入操作正式执行之前,首先会对文件进行备份,将初始文件重命名为增加了一个.bak后缀的备份文件:
// 尝试写入文件
private void writeToFile(...) {
    if (!backupFileExists) {
        !mFile.renameTo(mBackupFile);
    }
}
  1. 尝试对文件进行写入操作,写入成功时,则将备份文件删除:
// 写入成功,立即删除存在的备份文件
// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();
  1. 若因异常情况(比如进程被杀)导致写入失败,进程再次启动后,若发现存在备份文件,则将备份文件重名为源文件,原本未完成写入的文件就直接丢弃:
// 从磁盘初始化加载时执行
private void loadFromDisk() {
  synchronized (mLock) {
      if (mBackupFile.exists()) {
          mFile.delete();
          mBackupFile.renameTo(mFile);
      }
  }
}

通过文件备份机制,我们能够保证数据只会丢失最后的更新,而之前成功保存的数据依然能够有效。

进程不安全

SharedPreferences是进程不安全的,在跨进程频繁读写可能会有数据丢失

问题:这里如何解决?

ANR

写操作时ANR

Android系统在页面切换前,将数据写入文件。在ActivityThread的handlePauseActivity和handleStopActivity时会通过waitToFinish保证这些异步任务都已经被执行完成。如果这个时候过渡使用apply方法,则可能导致onpause,onStop执行时间较长,从而导致ANR。

private void handlePauseActivity(IBinder token, boolean finished,
          boolean userLeaving, int configChanges, boolean dontReport, int seq) {
     ......
          r.activity.mConfigChangeFlags |= configChanges;
          performPauseActivity(token, finished, r.isPreHoneycomb(), "handlePauseActivity");

          // Make sure any pending writes are now committed.
          if (r.isPreHoneycomb()) {
              QueuedWork.waitToFinish();
          }
         ......
  }
public static void waitToFinish() {
      Handler handler = getHandler();
      try {
          processPendingWork();
      } finally {
          StrictMode.setThreadPolicy(oldPolicy);
      }

      try {
          while (true) {
              Runnable finisher;

              synchronized (sLock) {
                  finisher = sFinishers.poll();
              }

              if (finisher == null) {
                  break;
              }

              finisher.run();
          }
      } finally {
          sCanDelay = true;
      }
  }
  1. 主线程执行processPendingWork()方法,把之前未执行完的内容存储到文件的操作执行完,这部分动作直接在主线程执行,如果有未执行的文件操作并且文件较大,则主线程会因为IO时间长造成ANR。
  2. 循环取出sFinishers数组,执行他的run方法。如果这时候有多个异步线程或者异步线程时间过长,同样会造成阻塞产生ANR。

sFinishers数组是在什么时候add数据的? sFinishers的addFinisher方法是在apply()方法里面调用的,代码如下:

@Override
public void apply() {
  ......
    // 将 awaitCommit 添加到队列 QueuedWork 中
    QueuedWork.addFinisher(awaitCommit);

  Runnable postWriteRunnable = new Runnable() {
    @Override
    public void run() {
      awaitCommit.run();
      QueuedWork.removeFinisher(awaitCommit);// 将 awaitCommit 从队列 QueuedWork 中移除
    }
  };
  ......
}

由于UI线程循环取出sFinishers数组,执行他的run方法造成阻塞产生ANR addFinisher刚刚上面提到是在apply方法中调用,入参awaitCommit,他的run方法如下:

final Runnable awaitCommit = new Runnable() {
  @Override
  public void run() {
    try {
      mcr.writtenToDiskLatch.await();//阻塞
    } catch (InterruptedException ignored) {
    }
    if (DEBUG && mcr.wasWritten) {
      Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
            + " applied after " + (System.currentTimeMillis() - startTime)
            + " ms");
    }
  }
};
						  

不难看出,就是调用CountDownLatch对象的await方法,阻塞当前线程,将当前线程加入阻塞队列。所以如果过渡使用apply方法,则可能导致onpause,onStop执行时间较长从而导致ANR。

sp-第 1 页.png

首次读操作时ANR

sp 文件创建以后,会单独的使用一个线程来加载解析对应的 sp 文件。但是当 UI 线程尝试访问 sp 中内容时,如果 sp 文件还未被完全加载解析到内存,此时 UI 线程会被 block,直到 SP 文件被完全加载到内存中为止

sp 被创建的时候会同时启动一个线程加载对应的 sp 文件,执行 startLoadFromDisk(); 在 startLoadFromDisk()时,标记 sp 不可使用状态,后期无论是尝试读数据或者写数据,读写线程都会被 block,直到 sp 文件被全部加载解析完毕。

源码
@UnsupportedAppUsage
    SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    mThrowable = null;
    startLoadFromDisk();
}

@UnsupportedAppUsage
private void startLoadFromDisk() {
    synchronized (mLock) {
    mLoaded = false;
    }
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}

sp 文件完全加载解析到内存中,直接唤起所有在等待在当前 sp 的读写线程。

private void loadFromDisk() {
    synchronized (mLock) {

    try {
        stat = Os.stat(mFile.getPath());
        if (mFile.canRead()) {
            BufferedInputStream str = null;
            try {
                str = new BufferedInputStream(
                        new FileInputStream(mFile), 16 * 1024);
                map = (Map<String, Object>) XmlUtils.readMapXml(str);
            } 
          }
    } 

    synchronized (mLock) {
        mLoaded = true;
        mLock.notifyAll();

    }
}

当UI线程读取SP的值时,可能会发生ANR;

@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

@GuardedBy("mLock")
private void awaitLoadedLocked() {
    if (!mLoaded) {
        // Raise an explicit StrictMode onReadFromDisk for this
        // thread, since the real read will be in a different
        // thread and otherwise ignored by StrictMode.
        BlockGuard.getThreadPolicy().onReadFromDisk();
    }
    while (!mLoaded) {
        try {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
    if (mThrowable != null) {
        throw new IllegalStateException(mThrowable);
    }
}

总结

项目中用DataStore或者MMKV去替换Sp存储,但是作为andoid最基础的数据存储,我们还是应该去了解Sp相关的源码。这样再学习新技术时,才能知道它是通过什么手段解决了什么问题。