赞
踩
SharedPreferences 是 Android 中非常重要的轻量级 KV 存储组件,但是 SharedPreferences 也有其性能方面的问题,本文将主要从源码的角度分析 SharedPreferences 的工作原理和存在的问题。
由于业务不断的发展,SharedPreferences 组件开始慢慢出现一些性能或者功能的缺陷,主要表现如下:
性能瓶颈
Google 将 SharedPreferences 作为轻量级 KV 存储组件推出,可是随着业务不断的发展,存储的 KV 越来越多,导致出现了性能上的问题,其中 commit 和 apply 方法均存在性能问题,文件的格式为XML,读写均较耗时。
存储安全问题
SharedPreferences 将 KV 存储在 XML 结构的文件中,没有加解密存储的功能,存在安全问题。
多进程问题
SharedPreferences 不支持跨进程同步问题,KV 组件跨进程是比较强的需求。
下面结合 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 个部分分析:
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 的加载状态,如果没有加载完成就等待,上面加载流程也分析过了,加载完成后会唤醒所有等待的线程。
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() { @Override public 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); }
这段代码基本就看上面标注的两个地方:
所以 commit 方法是同步写数据到文件中,会阻塞当前线程。继续看下 apply 方法:
public void apply() { final MemoryCommitResult mcr = commitToMemory(); final Runnable awaitCommit = new Runnable() { @Override public void run() { try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException ignored) { } ...... } }; QueuedWork.addFinisher(awaitCommit); Runnable postWriteRunnable = new Runnable() { @Override public 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 的生命周期中强行执行提交:
@Override
public 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 的出现。
Google 也一直在优化 SharedPreferences 的性能,但是 SharedPreferences 实在是跟不上现在 Android 项目的性能要求和功能要求,索性就开发新的框架 DataStore 来替代。SharedPreferences 中个人主要的收获是 BackupFile 的思路,当一个文件要频繁被读写的时候,就可能出现文件损坏的情况发生(例如数据库),这个时候就需要备份文件的设计,确保文件数据尽量不被丢失。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。