SharedPreferences学习笔记
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的源码,确实感觉自己对整个流程和使用都有了更加清晰的认识,也大概了解了如果以后想优化该从什么方面着手。在整个阅读的过程中,感觉自己对线程的同步、异步、锁的知识等等了解了还不够多不够深刻,需要以后加强。
作者:用户553955049176
链接:https://juejin.cn/post/7025865068600360967