阅读 107

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


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐