1. 背景
SharedPreferences 是 Android 中非常重要的轻量级 KV 存储组件,但是 SharedPreferences 也有其性能方面的问题,本文将主要从源码的角度分析 SharedPreferences 的工作原理和存在的问题。
2. 存在的问题
由于业务不断的发展,SharedPreferences 组件开始慢慢出现一些性能或者功能的缺陷,主要表现如下:
性能瓶颈
Google 将 SharedPreferences 作为轻量级 KV 存储组件推出,可是随着业务不断的发展,存储的 KV 越来越多,导致出现了性能上的问题,其中 commit 和 apply 方法均存在性能问题,文件的格式为XML,读写均较耗时。
存储安全问题
SharedPreferences 将 KV 存储在 XML 结构的文件中,没有加解密存储的功能,存在安全问题。
多进程问题
SharedPreferences 不支持跨进程同步问题,KV 组件跨进程是比较强的需求。
3. SharedPreferences 源码分析
下面结合 SharedPreferences 的源码分析其工作过程,并且指出其性能问题的根本原因,本文重点分析其性能相关的问题,分析源码的时候也主要关注这块。
3.1 SharedPreferences 加载
SharedPreferences 具体的实现在 SharedPreferencesImpl 中,初始化代码如下:
@UnsupportedAppUsage
SharedPreferencesImpl(File file, int mode) {mFile = file;mBackupFile = makeBackupFile(file);mMode = mode;mLoaded = false;mMap = null;mThrowable = null;startLoadFromDisk();
}
其中 mFile 是 SharedPreferences 存储的文件,mBackupFile 是存储文件的备份,主要在更新文件时起作用,防止系统问题 mFile 写不完整导致的数据丢失问题,重点看 startLoadFromDisk() 加载文件的函数。
private void startLoadFromDisk() {synchronized (mLock) {mLoaded = false;}new Thread("SharedPreferencesImpl-load") {public void run() {loadFromDisk();}}.start();}
通过 mLock 锁管理文件加载状态 mLoaded,这里开启线程 loadFromDisk() , 主要是这里并不一定要立马使用 SharedPreferences,尽可能减少对调用线程的阻塞,继续看 loadFromDisk() 方法
private void loadFromDisk() {(1)synchronized (mLock) {if (mLoaded) {return;}if (mBackupFile.exists()) {mFile.delete();mBackupFile.renameTo(mFile);}}......try {(2)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);} catch (Exception e) {Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);} finally {IoUtils.closeQuietly(str);}}} catch (Throwable t) {thrown = t;}(3)synchronized (mLock) {mLoaded = true;mThrowable = thrown;try {.......} catch (Throwable t) {mThrowable = t;} finally {mLock.notifyAll();}}}
loadFromDisk() 方法整体逻辑分成 3 个部分分析:
- 这里有个很关键的 mBackupFile 的判断和使用,SharedPreferences 在更新数据到文件的时候会将原来的文件重命名为 mBackupFile,然后将所有的 KV 全量写入文件中,执行完成后删除 mBackupFile,如果 mBackupFile 存在就说明之前全量写可能失败了,mFile 可能有损坏,所以需要删除 mFile,使用 mBackupFile 。
- 主要是从 XML 格式的文件中读取数据并且解析成 map 。
- 修改 mLoaded 加载状态,并且唤醒所有等待的线程,这些等待的线程后面说。
3.2 SharedPreferences 读取数据
SharedPreferences 读取数据有很多种方法,基本都是封装过的,这里看下其中的 getString 方法。
public String getString(String key, @Nullable String defValue) {synchronized (mLock) {awaitLoadedLocked();String v = (String)mMap.get(key);return v != null ? v : defValue;}
}
这个方法看起来就是一个阻塞等待 SharedPreferences 加载完成后从 mMap 种获取数据的过程,重点看下 awaitLoadedLocked() 方法的实现。
private void awaitLoadedLocked() {......while (!mLoaded) {try {mLock.wait();} catch (InterruptedException unused) {}}if (mThrowable != null) {throw new IllegalStateException(mThrowable);}}
这里就是判断 SharedPreferences 的加载状态,如果没有加载完成就等待,上面加载流程也分析过了,加载完成后会唤醒所有等待的线程。
3.3 SharedPreferences 存储数据
SharedPreferences 使用 Editor 中的 putXXX 系列方法修改数据,最后通过 commit 或者 apply 方法提交修改。这里可以看出SharedPreferences 最初的设计就是希望多次修改后统一提交。先看一下 commit 的源码
public boolean commit() {......(1)MemoryCommitResult mcr = commitToMemory();(2)SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null);try {(3)mcr.writtenToDiskLatch.await();} catch (InterruptedException e) {return false;} finally {......}notifyListeners(mcr);return mcr.writeToDiskResult;
}
这里主要的逻辑就有三部分:
- 对比确定当前内存中修改的需要提交的内容。
- 添加到写数据的队列中。
- 阻塞当前线程等待写数据结束。
看下 MemoryCommitResult 的定义:
private static class MemoryCommitResult {......final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);......void setDiskWriteResult(boolean wasWritten, boolean result) {......writtenToDiskLatch.countDown();}}
上面省略了大部分内容,可以看出在写数据结束后才会唤醒阻塞的线程。
下面看下 SharedPreferencesImpl.this.enqueueDiskWrite 的内容:
private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {final boolean isFromSyncCommit = (postWriteRunnable == null);final Runnable writeToDiskRunnable = new Runnable() {@Overridepublic void run() {synchronized (mWritingToDiskLock) {(1)writeToFile(mcr, isFromSyncCommit);}synchronized (mLock) {mDiskWritesInFlight--;}if (postWriteRunnable != null) {postWriteRunnable.run();}}};if (isFromSyncCommit) {boolean wasEmpty = false;synchronized (mLock) {wasEmpty = mDiskWritesInFlight == 1;}if (wasEmpty) {writeToDiskRunnable.run();return;}}(2)QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);}
这段代码基本就看上面标注的两个地方:
- 写数据到文件中,这里面的内容不再深入了。
- QueuedWork.queue 可以简单理解为一个延迟 100 毫秒单独执行的线程。
所以 commit 方法是同步写数据到文件中,会阻塞当前线程。继续看下 apply 方法:
public void apply() {final MemoryCommitResult mcr = commitToMemory();final Runnable awaitCommit = new Runnable() {@Overridepublic void run() {try {mcr.writtenToDiskLatch.await();} catch (InterruptedException ignored) {}......}};QueuedWork.addFinisher(awaitCommit);Runnable postWriteRunnable = new Runnable() {@Overridepublic void run() {awaitCommit.run();QueuedWork.removeFinisher(awaitCommit);}};SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);......}
apply 方法中的 mcr.writtenToDiskLatch.await() 看起来没有阻塞当前的线程,SharedPreferencesImpl.this.enqueueDiskWrite 方法前面分析过是直接调用 QueuedWork.queue 放到另外一个线程中执行。
上面看着是不是 apply 方法没啥问题,提交到线程中执行,不会阻塞当前的线程,也就没有主线程的性能问题了吗?Android 为了提高 apply 方法的提交成功率在 Activity 和 Service 的生命周期中强行执行提交:
@Overridepublic void handleStopActivity(ActivityClientRecord r, int configChanges,PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {......if (!r.isPreHoneycomb()) {QueuedWork.waitToFinish();}......}
private void handleStopService(IBinder token) {mServicesData.remove(token);Service s = mServices.remove(token);if (s != null) {try {s.onDestroy();......QueuedWork.waitToFinish();......} catch (Exception e) {......}} else {Slog.i(TAG, "handleStopService: token=" + token + " not found.");}}
这两个方法均会执行 QueuedWork.waitToFinish() 方法,QueuedWork.waitToFinish() 中会将所有提交的任务阻塞式执行完,所以就可能导致 ANR 的出现。
4. SharedPreferences 源码总结
- SharedPreferences 读写的时候都是阻塞状态,直到文件加载完成。
- commit 和 apply 方法均可以提交修改,commit 是同步的,apply 是异步的。
- commit 和 apply 的提交均存在性能问题。
5. KV 组件使用的建议
- 如果要使用 SharedPreferences 作为 KV 组件,要注意多次修改统一提交,减少修改文件次数,同一个 SharedPreferences 存储的数据不应该过多。
- 可以使用 MMKV 等高性能的 KV 组件。
6. 框架学习总结
Google 也一直在优化 SharedPreferences 的性能,但是 SharedPreferences 实在是跟不上现在 Android 项目的性能要求和功能要求,索性就开发新的框架 DataStore 来替代。SharedPreferences 中个人主要的收获是 BackupFile 的思路,当一个文件要频繁被读写的时候,就可能出现文件损坏的情况发生(例如数据库),这个时候就需要备份文件的设计,确保文件数据尽量不被丢失。