SharedPreference是用于访问和修改Context.getSharedPreferences返回数据的接口。它是谷歌官方实现的轻量级数据存储的方案,可以用来保存用户信息等等数据。sp提供了很高的一致性保障,但是代价也很高,它很可能会导致anr问题。
SharedPreferences的执行流程
SharedPreferences的获取
我们平常都调用context的getSharedPreferences,这个方法实际是由ContextImpl类实现的。调用它会返回一个SharedPreferencesImpl,对应参数中的文件名。而对于每一个特定的xml文件,所有客户端都对应着同一个sp实例。这种对应关系放在ArrayMap里保存。如果ArrayMap中已经有sp了就直接返回,只有没有的情况下会新建一个sp实例并保存进ArrayMap中。
// getSharedPreferences(String name,int mode)实际上调用 (File file, int mode))
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
// ...
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
// ...
}
return sp;
}
// packagePrefs key:File val:SharedPreferencesImpl 文件和sp一一对应
// sSharedPrefsCache key:packageName val:packagePrefs 包名和上面arraymap一一对应
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
final String packageName = getPackageName();
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}
读取数据
SharedPreferencesImpl是SharedPreferences的默认实现类。在被new出来的时候,就调用startLoadFromDisk将k-v对从磁盘上读入内存,为了保证安全的load,使用了mlock做为锁。通过异步的loadFromDisk进程来读取数据,mloaded标志加载是否完成。loadFromDisk的逻辑是如果有备份文件,就用备份文件代替传进来的文件,然后读取kv对存到一个临时的map里去,最终如果没有异常的话再把它赋值给全局的mMap。
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
// Debugging
if (mFile.exists() && !mFile.canRead()) {
Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
}
Map<String, Object> map = null;
StructStat stat = null;
Throwable thrown = null;
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);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
} catch (Throwable t) {
thrown = t;
}
synchronized (mLock) {
mLoaded = true;
mThrowable = thrown;
try {
if (thrown == null) {
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
}
} catch (Throwable t) {
mThrowable = t;
} finally {
mLock.notifyAll();
}
}
}
mMap存储k-v键值对,加载完成后无论成功还是失败,都调用mLock.notifyAll()来唤醒所有等待的读进程和editor对象。
// SharedPreferences的getXXX方法都要调用该方法来保证异步load已经完成了
private void awaitLoadedLocked() {
if (!mLoaded) {
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
mLock.wait(); // 等待notifyAll唤醒
} catch (InterruptedException unused) {
}
}
//...
}
public Map<String, ?> getAll() {
synchronized (mLock) {
awaitLoadedLocked();
return new HashMap<String, Object>(mMap);
}
}
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
修改数据
为了保持数据保证一致的状态,SharedPreferences内置了一个Editor类来操作一切修改。修改的内容先放入map中存起来,只有commit或apply时才会真正与文件进行操作。
private final Object mEditorLock = new Object(); // 编辑锁
private final Map<String, Object> mModified = new HashMap<>();
private boolean mClear = false;
// mModified保存修改的k-v mClear标志是否清空
// 删除键的话 mModified.put(key,this) 做为标志
public Editor putXXX(String key, XXX value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
commit和apply的主要区别在于commit是同步执行的,而apply是异步的可以delay的。可以看到apply和commit的主要流程都是先获得一个MemoryCommitResult对象,然后调用enqueueDiskWrite放入队列中执行,可以看到postWriteRunnable是否为空就是enqueueDiskWrite判断调用方法是commit还是apply的关键。apply的awaitCommit被放入了QueueWork中保证在类似Activity的onstop调用时会去执行,确保数据不会丢失。
public void apply() {
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await(); //等到countdown计数为0之后这个线程才能执行
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
public boolean commit() {
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
其实commitToMemory主要是用于生成MemoryCommitResult,包括几个重要变量mapToWriteToDisk,memoryStateGeneration ,keysCleared ,keysModified 用于存入磁盘时的处理。mDiskWritesInFlight标志了当前有几个线程在执行提交操作,因此commitToMemory时它会++,等到writeToFile完毕时,它又会--.memoryStateGeneration是用于标志内存sp的版本,通过比对它和磁盘sp版本,全局的memory sp版本就能确定是否要commit或者apply。
writtenToDiskLatch 是一个倒数计时器,在apply的awaitCommit中它的await()方法就是等待setDiskWriteResult方法执行将计数器减一,使writtenToDiskLatch所在的主线程可以执行。
private static class MemoryCommitResult {
final long memoryStateGeneration;
final boolean keysCleared;
@Nullable final List<String> keysModified;
@Nullable final Set<OnSharedPreferenceChangeListener> listeners;
final Map<String, Object> mapToWriteToDisk;
final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
@GuardedBy("mWritingToDiskLock")
volatile boolean writeToDiskResult = false;
boolean wasWritten = false;
private MemoryCommitResult(long memoryStateGeneration, boolean keysCleared,
@Nullable List<String> keysModified,
@Nullable Set<OnSharedPreferenceChangeListener> listeners,
Map<String, Object> mapToWriteToDisk) {
// ...
}
// writeToFile 会执行
void setDiskWriteResult(boolean wasWritten, boolean result) {
this.wasWritten = wasWritten;
writeToDiskResult = result;
writtenToDiskLatch.countDown();
}
}
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
boolean keysCleared = false;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
if (mDiskWritesInFlight > 0) {
mMap = new HashMap<String, Object>(mMap);
}
mapToWriteToDisk = mMap;
mDiskWritesInFlight++; //又多了一个在修改提交的线程
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList<String>();
listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (mEditorLock) {
// mapToWriteToDisk 的设置逻辑
if (changesMade) {
mCurrentMemoryStateGeneration++; // 全局的
}
// 多个进程操作 在writeToFile执行时,memoryStateGeneration和
// mCurrentMemoryStateGeneration并不一定相同
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
listeners, mapToWriteToDisk);
}
在enqueueDiskWrite,根据mDiskWritesInFlight也就是进行提交的线程个数来判断该怎么执行,如果时commit并且mDiskWritesInFlight=1,那么本线程直接可以执行,否则的话放入队列执行。真正执行的函数就是writeToFile,然后如果是apply的话postWriteRunnable阻塞线程等待结果。
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) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--; // 提交完 diskwrite的进程就减1
}
if (postWriteRunnable != null) {
postWriteRunnable.run(); // awaitCommit等待结果返回
}
}
};
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1; // 只剩一个并且是commit可以直接本进程执行
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
// 否则放入队列执行
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
writeToFile的逻辑主要可以分成下面几个步骤:
- 首先设置needsWrite的布尔值, 首先只有磁盘sp版本比较低才需要write,然后看看是不是commit,或者apply时mcr的sp版本和全局最新的版本一致才更新,否则不需要太频繁的操作。
- 在needsWrite为true的情况下,看看有没有备份文件,备份文件主要是在修改失败时恢复到上一个数据一致性状态用的,所以如果没有备份文件,就用mFile创建一个,有的话mFile就可以删了,备份文件的设置出错的话,函数就直接返回了。
- 修改文件去,修改成功的话删除备份文件,并且更新各种值比如磁盘sp版本等等。调用setDiskWriteResult返回结果,这个函数内执行了倒数计时器的countdown操作,使postWriteRunnable不会再阻塞。
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
long startTime = 0;
long existsTime = 0;
long backupExistsTime = 0;
long outputStreamCreateTime = 0;
long writeTime = 0;
long fsyncTime = 0;
long setPermTime = 0;
long fstatTime = 0;
long deleteTime = 0;
boolean fileExists = mFile.exists();
if (fileExists) {
boolean needsWrite = false;
if (mDiskStateGeneration < mcr.memoryStateGeneration) {
if (isFromSyncCommit) {
needsWrite = true;
} else {
synchronized (mLock) {
if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
needsWrite = true;
}
}
}
}
if (!needsWrite) {
mcr.setDiskWriteResult(false, true);
return;
}
boolean backupFileExists = mBackupFile.exists();
if (!backupFileExists) {
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
mcr.setDiskWriteResult(false, false);
return;
}
} else {
mFile.delete();
}
}
try {
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
mcr.setDiskWriteResult(false, false);
return;
}
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
writeTime = System.currentTimeMillis();
FileUtils.sync(str);
fsyncTime = System.currentTimeMillis();
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
if (DEBUG) {
setPermTime = System.currentTimeMillis();
}
try {
final StructStat stat = Os.stat(mFile.getPath());
synchronized (mLock) {
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
}
} catch (ErrnoException e) {
}
mBackupFile.delete();
mDiskStateGeneration = mcr.memoryStateGeneration;
mcr.setDiskWriteResult(true, true);
long fsyncDuration = fsyncTime - writeTime;
mSyncTimes.add((int) fsyncDuration);
mNumSync++;
return;
} catch (XmlPullParserException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
} catch (IOException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
}
if (mFile.exists()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
mcr.setDiskWriteResult(false, false);
}
SharedPreferences的问题
在实际使用的情况下,确实发现用了sharedpreferences之后会感觉有点卡顿,我的模拟机比较明显。在阅读sp的源码和别人的博客之后发现,sp在加载和writeToFile时候都是一次性读取整个文件或重写整个文件的,如果xml比较大的话,就会影响性能。
同时由于apply方法会调用QueuedWork.addFinisher(awaitCommit),awaitCommit就是阻塞线程等待writeToFile返回,而在activity.onstop、broadcastReceiver.onReceive、service.handleCommend的时候,会调用QueuedWork.waitToFinish()去执行queuework里面所有的work和finisher。这样的话在activity.onstop的时候就会花很长时间等待,会影响到activtiy生命周期的改变,造成anr问题。
不过为啥一定要调用waitToFinsh还没有很理解,而且在其他人的博客里也看到通过直接清空queuework的等待队列减少anr的产生。
改进的想法
- 可以自定义SharedPreferences的实现类
- 调用waitToFinsh方法之前,清空等待队列
- 学习使用mmkv
感想
学习了sharedpreferences的源码,确实感觉自己对整个流程和使用都有了更加清晰的认识,也大概了解了如果以后想优化该从什么方面着手。在整个阅读的过程中,感觉自己对线程的同步、异步、锁的知识等等了解了还不够多不够深刻,需要以后加强。