SharedPreferences
應該是任何一名 Android 初學者都知道的存儲類了,它輕量,適合用於保存軟件配置等參數。以鍵值對的 XML 文件形式存儲在本地,程序卸載後也會一併清除,不會殘留信息。java
使用起來也很是簡單。android
// 讀取 val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE) val string = sharedPreferences.getString("123","") // 寫入 val editor = sharedPreferences.edit() editor.putString("123","123") editor.commit()
當咱們寫下這樣的代碼的時候,IDE 極易出現一個警告,提示咱們用 apply()
來替換 commit()
。緣由也很簡單,由於 commit()
是同步的,而 apply()
採用異步的方式一般來講效率會更高一些。可是,當咱們把 editor.commit()
的返回值賦給一個變量的時候,這時候就會發現 IDE 沒有了警告。這是由於 IDE 認爲咱們想要使用 editor.commit()
的返回值了,因此,一般來講,在咱們不關心操做結果的時候,咱們更傾向於使用 apply()
進行寫入的操做。c++
咱們能夠經過 3 種方式來獲取 SharedPreferences
的實例。
首先固然是咱們最多見的寫法。c#
getSharedPreferences("123", Context.MODE_PRIVATE)
Context
的任意子類均可以直接經過 getSharedPreferences()
方法獲取到 SharedPreferences
的實例,接受兩個參數,分別對應 XML 文件的名字和操做模式。其中 MODE_WORLD_READABLE
和 MODE_WORLD_WRITEABLE
這兩種模式已在 Android 4.2 版本中被廢棄。api
SharedPreferences
數據只能被本應用程序讀、寫;SharedPreferences
數據能被其餘應用程序讀,但不能寫;SharedPreferences
數據能被其餘應用程序讀;另外在 Activity
的實現中,還能夠直接經過 getPreferences()
獲取,實際上也就把當前 Activity 的類名做爲文件名參數。緩存
public SharedPreferences getPreferences(@Context.PreferencesMode int mode) { return getSharedPreferences(getLocalClassName(), mode); }
此外,咱們也能夠經過 PreferenceManager
的 getDefaultSharedPreferences()
獲取到。安全
public static SharedPreferences getDefaultSharedPreferences(Context context) { return context.getSharedPreferences(getDefaultSharedPreferencesName(context), getDefaultSharedPreferencesMode()); } public static String getDefaultSharedPreferencesName(Context context) { return context.getPackageName() + "_preferences"; } private static int getDefaultSharedPreferencesMode() { return Context.MODE_PRIVATE; }
能夠很明顯的看到,這個方式就是在直接把當前應用的包名做爲前綴來進行命名的。多線程
注意:若是在 Fragment 中使用
SharedPreferences
時,SharedPreferences
的初始化儘可能放在onAttach(Activity activity)
裏面進行 ,不然可能會報空指針,即getActivity()
會可能返回爲空。app
有較多 SharedPreferences
使用經驗的人,就會發現 SharedPreferences
其實具有挺多的坑,但這些坑主要都是由於不熟悉其中真正的原理所致使的,因此,筆者在這裏,帶你們一塊兒揭開 SharedPreferences
的神祕面紗。異步
前面講了 SharedPreferences
有三種獲取實例的方法,但歸根結底都是調用的 Context
的 getSharedPreferences()
方法。因爲 Android 的 Context
類採用的是裝飾者模式,而裝飾者對象其實就是 ContextImpl
,因此咱們來看看源碼是怎麼實現的。
// 存放的是名稱和文件夾的映射,實際上這個名稱就是咱們外面傳進來的 name private ArrayMap<String, File> mSharedPrefsPaths; public SharedPreferences getSharedPreferences(String name, int mode) { // At least one application in the world actually passes in a null // name. This happened to work because when we generated the file name // we would stringify it to "null.xml". Nice. if (mPackageInfo.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.KITKAT) { if (name == null) { name = "null"; } } File file; synchronized (ContextImpl.class) { if (mSharedPrefsPaths == null) { mSharedPrefsPaths = new ArrayMap<>(); } file = mSharedPrefsPaths.get(name); if (file == null) { file = getSharedPreferencesPath(name); mSharedPrefsPaths.put(name, file); } } return getSharedPreferences(file, mode); } @Override public File getSharedPreferencesPath(String name) { return makeFilename(getPreferencesDir(), name + ".xml"); } private File makeFilename(File base, String name) { if (name.indexOf(File.separatorChar) < 0) { return new File(base, name); } throw new IllegalArgumentException( "File " + name + " contains a path separator"); }
能夠很明顯的看到,內部是採用 ArrayMap
來作的處理,而這個 mSharedPrefsPaths
主要是用於存放名稱和文件夾的映射,實際上這個名稱就是咱們外面傳進來的 name,這時候咱們經過 name 拿到咱們的 File,若是當前池子中沒有的話,則直接新建一個 File,並放入到 mSharedPrefsPaths
中。最後仍是調用的重載方法 getSharedPreferences(File,mode)
// 存放包名與ArrayMap鍵值對,初始化時會默認以包名做爲鍵值對中的 Key,注意這是個 static 變量 private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache; @Override 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) { checkMode(mode); if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) { if (isCredentialProtectedStorage() && !getSystemService(UserManager.class) .isUserUnlockingOrUnlocked(UserHandle.myUserId())) { throw new IllegalStateException("SharedPreferences in credential encrypted " + "storage are not available until after user is unlocked"); } } sp = new SharedPreferencesImpl(file, mode); cache.put(file, sp); return sp; } } if ((mode & Context.MODE_MULTI_PROCESS) != 0 || getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) { // If somebody else (some other process) changed the prefs // file behind our back, we reload it. This has been the // historical (if undocumented) behavior. sp.startReloadIfChangedUnexpectedly(); } return sp; } 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; }
能夠看到,又採用了一個 ArrayMap
來存放文件和 SharedPreferencesImpl
組成的鍵值對,而後經過經過單例的方式返回一個 SharedPreferences
對象,其實是 SharedPreferences
的實現類 SharedPreferencesImpl
,並且在其中還創建了一個內部緩存機制。
因此,從上面的分析中,咱們能知道 對於一個相同的 name,咱們獲取到的都是同一個 SharedPreferencesImpl 對象。
在上面的操做中,咱們能夠看到在第一次調用 getSharedPreferences
的時候,咱們會去構造一個 SharedPreferencesImpl
對象,咱們來看看都作了什麼。
SharedPreferencesImpl(File file, int mode) { mFile = file; mBackupFile = makeBackupFile(file); mMode = mode; mLoaded = false; mMap = null; mThrowable = null; startLoadFromDisk(); } 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) { // An errno exception means the stat failed. Treat as empty/non-existing by // ignoring. } catch (Throwable t) { thrown = t; } synchronized (mLock) { mLoaded = true; mThrowable = thrown; // It's important that we always signal waiters, even if we'll make // them fail with an exception. The try-finally is pretty wide, but // better safe than sorry. try { if (thrown == null) { if (map != null) { mMap = map; mStatTimestamp = stat.st_mtim; mStatSize = stat.st_size; } else { mMap = new HashMap<>(); } } // In case of a thrown exception, we retain the old map. That allows // any open editors to commit and store updates. } catch (Throwable t) { mThrowable = t; } finally { mLock.notifyAll(); } } }
注意看咱們的 startLoadFromDisk
方法,咱們會去新開一個子線程,而後去經過 XmlUtils.readMapXml()
方法把指定的 SharedPreferences
文件的全部的鍵值對都讀出來,而後存放到一個 map 中。
而衆所周知,文件的讀寫操做都是耗時的,可想而知,在咱們第一次去讀取一個 SharedPreferences
文件的時候花上了太多的時間會怎樣。
上面講了初次獲取一個文件的 SharedPreferences
實例的時候,會先去把全部鍵值對讀取到緩存中,這明顯是一個耗時操做,而咱們正常的去讀取數據的時候,都是相似這樣的代碼。
val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE) val string = sharedPreferences.getString("123","")
SharedPreferences
的getXXX()
方法可能會報ClassCastException
異常,因此咱們在同一個 name 的時候,對不同的類型,必須使用不一樣的 key。可是putXXX
是能夠用不一樣的類型值覆蓋相同的 key 的。
那勢必可能會致使這個操做須要等待必定的時間,咱們姑且能夠這麼猜測,在 getXXX()
方法執行的時候應該是會等待前面的操做完成才能執行的。
由於 SharedPreferences
是一個接口,因此咱們主要來看看它的實現類 SharedPreferencesImpl
,這裏以 getString()
爲例。
@Override @Nullable public String getString(String key, @Nullable String defValue) { synchronized (mLock) { awaitLoadedLocked(); String v = (String)mMap.get(key); return v != null ? v : defValue; } }
awaitLoadedLocked()
方法應該就是咱們所想的等待執行操做了,咱們看看裏面作了什麼。
private void awaitLoadedLocked() { if (!mLoaded) { // Raise an explicit StrictMode onReadFromDisk for this // thread, since the real read will be in a different // thread and otherwise ignored by StrictMode. BlockGuard.getThreadPolicy().onReadFromDisk(); } while (!mLoaded) { try { mLock.wait(); } catch (InterruptedException unused) { } } if (mThrowable != null) { throw new IllegalStateException(mThrowable); } }
能夠看到,在 awaitLoadedLocked
方法裏面咱們使用了 mLock.wait()
來等待初始化的讀取操做,而咱們前面看到的 loadFromDiskLocked()
方法的最後也能夠看到它調用了 mLock.notifyAll()
方法來喚醒後面這個阻塞的 getXXX()
。那麼這裏就會明顯出現一個問題,咱們的 getXXX()
方法是寫在 UI 線程的,若是這個方法被阻塞的過久,勢必會出現 ANR 的狀況。因此咱們必定在平時須要根據具體狀況考慮是否須要把 SharedPreferences
的讀寫操做放在子線程中。
咱們在寫入數據以前,老是要先經過相似這樣的代碼獲取 SharedPreferences
的內部類 Editor
。
val editor = sharedPreferences.edit()
咱們固然要看看這個究竟是什麼東西。
@Override public Editor edit() { // TODO: remove the need to call awaitLoadedLocked() when // requesting an editor. will require some work on the // Editor, but then we should be able to do: // // context.getSharedPreferences(..).edit().putString(..).apply() // // ... all without blocking. synchronized (mLock) { awaitLoadedLocked(); } return new EditorImpl(); }
咱們在
能夠看到,咱們在讀取解析完 XML 文件的時候,直接返回了一個 Editor
的實現類 EditorImpl
。咱們隨便查看一個 putXXX 的方法一看。
private final Object mEditorLock = new Object(); @GuardedBy("mEditorLock") private final Map<String, Object> mModified = new HashMap<>(); @GuardedBy("mEditorLock") private boolean mClear = false; @Override public Editor putString(String key, @Nullable String value) { synchronized (mEditorLock) { mModified.put(key, value); return this; } }
能夠看到,咱們在 EditorImpl
裏面使用了一個 HashMap
來存放咱們的鍵值對數據,每次 put 的時候都會直接往這個鍵值對變量 mModified
中進行數據的 put 操做。
咱們老是在更新數據後須要加上 commit()
或者 apply()
來進行輸入的寫入操做,咱們不妨來看看他們的實現到底有什麼區別。
先看 commit() 和 apply() 的源碼。
@Override public boolean commit() { long startTime = 0; if (DEBUG) { startTime = System.currentTimeMillis(); } MemoryCommitResult mcr = commitToMemory(); SharedPreferencesImpl.this.enqueueDiskWrite( mcr, null /* sync write on this thread okay */); try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException e) { return false; } finally { if (DEBUG) { Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration + " committed after " + (System.currentTimeMillis() - startTime) + " ms"); } } notifyListeners(mcr); return mcr.writeToDiskResult; } @Override public void apply() { final long startTime = System.currentTimeMillis(); final MemoryCommitResult mcr = commitToMemory(); final Runnable awaitCommit = new Runnable() { @Override public void run() { try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException ignored) { } if (DEBUG && mcr.wasWritten) { Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration + " applied after " + (System.currentTimeMillis() - startTime) + " ms"); } } }; QueuedWork.addFinisher(awaitCommit); Runnable postWriteRunnable = new Runnable() { @Override public void run() { awaitCommit.run(); QueuedWork.removeFinisher(awaitCommit); } }; SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); // Okay to notify the listeners before it's hit disk // because the listeners should always get the same // SharedPreferences instance back, which has the // changes reflected in memory. notifyListeners(mcr); }
能夠看到,apply()
和 commit()
的區別是在 commit()
把內容同步提交到了硬盤,而 apply()
是先當即把修改提交給了內存,而後開啓了一個異步的線程提交到硬盤。commit()
會接收 MemoryCommitResult
裏面的一個 boolean
參數做爲結果,而 apply()
沒有對結果作任何關心。
咱們能夠看到,文件寫入更新的操做都是交給 commitToMemory()
作的,這個方法返回了一個 MemoryCommitResult
對象,咱們來看看到底作了什麼。
// Returns true if any changes were made private MemoryCommitResult commitToMemory() { long memoryStateGeneration; List<String> keysModified = null; Set<OnSharedPreferenceChangeListener> listeners = null; Map<String, Object> mapToWriteToDisk; synchronized (SharedPreferencesImpl.this.mLock) { // We optimistically don't make a deep copy until // a memory commit comes in when we're already // writing to disk. if (mDiskWritesInFlight > 0) { // We can't modify our mMap as a currently // in-flight write owns it. Clone it before // modifying it. // noinspection unchecked 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) { boolean changesMade = false; if (mClear) { if (!mapToWriteToDisk.isEmpty()) { changesMade = true; mapToWriteToDisk.clear(); } mClear = false; } for (Map.Entry<String, Object> e : mModified.entrySet()) { String k = e.getKey(); Object v = e.getValue(); // "this" is the magic value for a removal mutation. In addition, // setting a value to "null" for a given key is specified to be // equivalent to calling remove on that key. if (v == this || v == null) { if (!mapToWriteToDisk.containsKey(k)) { continue; } mapToWriteToDisk.remove(k); } else { if (mapToWriteToDisk.containsKey(k)) { Object existingValue = mapToWriteToDisk.get(k); if (existingValue != null && existingValue.equals(v)) { continue; } } mapToWriteToDisk.put(k, v); } changesMade = true; if (hasListeners) { keysModified.add(k); } } mModified.clear(); if (changesMade) { mCurrentMemoryStateGeneration++; } memoryStateGeneration = mCurrentMemoryStateGeneration; } } return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners, mapToWriteToDisk); }
能夠看到,咱們這裏的 mMap
即存放當前 SharedPreferences
文件中的鍵值對,而 mModified
則存放的是當時 edit()
時 put 進去的鍵值對,這個咱們前面有所介紹。這裏有個 mDiskWritesInFlight
看起來應該是表示正在等待寫的操做數量。
接下來咱們首先處理了 edit().clear()
操做的 mClear
標誌,當咱們在外面調用 clear()
方法的時候,咱們會把 mClear
設置爲 true,這時候咱們會直接經過 mMap.clear()
清空此時文件中的鍵值對,而後再遍歷 mModified
中新 put 進來的鍵值對數據放到 mMap
中。也就是說:在一次提交中,若是咱們又有 put 又有 clear()
操做的話,咱們只能 clear()
掉以前的鍵值對,此次 put()
進去的鍵值對仍是會被寫入到 XML 文件中。
// 讀取 val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE) // 寫入 val editor = sharedPreferences.edit() editor.putInt("1", 123) editor.clear() editor.apply() Log.e("nanchen2251", "${sharedPreferences.getInt("1", 0)}")
也就是說,當咱們編寫下面的代碼的時候,獲得的打印仍是 123。
而後咱們接着往下看,又發現了另一個 commit()
和 apply()
都作了調用的方法是 enqueueDiskWrite()
。
/** * Enqueue an already-committed-to-memory result to be written * to disk. * * They will be written to disk one-at-a-time in the order * that they're enqueued. * * @param postWriteRunnable if non-null, we're being called * from apply() and this is the runnable to run after * the write proceeds. if null (from a regular commit()), * then we're allowed to do this disk write on the main * thread (which in addition to reducing allocations and * creating a background thread, this has the advantage that * we catch them in userdebug StrictMode reports to convert * them where possible to apply() ...) */ 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--; } if (postWriteRunnable != null) { postWriteRunnable.run(); } } }; // Typical #commit() path with fewer allocations, doing a write on // the current thread. if (isFromSyncCommit) { boolean wasEmpty = false; synchronized (mLock) { wasEmpty = mDiskWritesInFlight == 1; } if (wasEmpty) { writeToDiskRunnable.run(); return; } } QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit); }
在這個方法中,首先經過判斷 postWriteRunnable
是否爲 null 來判斷是 apply()
仍是 commit()
。而後定義了一個 Runnable
任務,在 Runnable
中先調用了 writeToFile()
進行了寫入和計數器更新的操做。
而後咱們再來看看這個 writeToFile()
方法作了些什麼。
@GuardedBy("mWritingToDiskLock") 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; if (DEBUG) { startTime = System.currentTimeMillis(); } boolean fileExists = mFile.exists(); if (DEBUG) { existsTime = System.currentTimeMillis(); // Might not be set, hence init them to a default value backupExistsTime = existsTime; } // Rename the current file so it may be used as a backup during the next read if (fileExists) { boolean needsWrite = false; // Only need to write if the disk state is older than this commit if (mDiskStateGeneration < mcr.memoryStateGeneration) { if (isFromSyncCommit) { needsWrite = true; } else { synchronized (mLock) { // No need to persist intermediate states. Just wait for the latest state to // be persisted. if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) { needsWrite = true; } } } } if (!needsWrite) { mcr.setDiskWriteResult(false, true); return; } boolean backupFileExists = mBackupFile.exists(); if (DEBUG) { backupExistsTime = System.currentTimeMillis(); } // 此處須要注意一下 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(); } } // Attempt to write the file, delete the backup and return true as atomically as // possible. If any exception occurs, delete the new file; next time we will restore // from the backup. try { FileOutputStream str = createFileOutputStream(mFile); if (DEBUG) { outputStreamCreateTime = System.currentTimeMillis(); } 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) { // Do nothing } if (DEBUG) { fstatTime = System.currentTimeMillis(); } // Writing was successful, delete the backup file if there is one. mBackupFile.delete(); if (DEBUG) { deleteTime = System.currentTimeMillis(); } mDiskStateGeneration = mcr.memoryStateGeneration; mcr.setDiskWriteResult(true, true); if (DEBUG) { Log.d(TAG, "write: " + (existsTime - startTime) + "/" + (backupExistsTime - startTime) + "/" + (outputStreamCreateTime - startTime) + "/" + (writeTime - startTime) + "/" + (fsyncTime - startTime) + "/" + (setPermTime - startTime) + "/" + (fstatTime - startTime) + "/" + (deleteTime - startTime)); } long fsyncDuration = fsyncTime - writeTime; mSyncTimes.add((int) fsyncDuration); mNumSync++; if (DEBUG || mNumSync % 1024 == 0 || fsyncDuration > MAX_FSYNC_DURATION_MILLIS) { mSyncTimes.log(TAG, "Time required to fsync " + mFile + ": "); } return; } catch (XmlPullParserException e) { Log.w(TAG, "writeToFile: Got exception:", e); } catch (IOException e) { Log.w(TAG, "writeToFile: Got exception:", e); } // Clean up an unsuccessfully written file if (mFile.exists()) { if (!mFile.delete()) { Log.e(TAG, "Couldn't clean up partially-written file " + mFile); } } mcr.setDiskWriteResult(false, false); }
代碼比較長,作了一些時間的記錄和 XML 的相關處理,但最值得咱們關注的仍是其中打了標註的對於 mBackupFile
的處理。咱們能夠明顯地看到,在咱們寫入文件的時候,咱們會把此前的 XML 文件更名爲一個備份文件,而後再將要寫入的數據寫入到一個新的文件中。若是這個過程執行成功的話,就會把備份文件刪除。因而可知:即便咱們每次只是添加一個鍵值對,也會從新寫入整個文件的數據,這也說明了 SharedPreferences 只適合保存少許數據,文件太大會有性能問題。
看完了這個 writeToFile()
,咱們再來看看下面作了啥。
// Typical #commit() path with fewer allocations, doing a write on // the current thread. if (isFromSyncCommit) { boolean wasEmpty = false; synchronized (mLock) { wasEmpty = mDiskWritesInFlight == 1; } if (wasEmpty) { writeToDiskRunnable.run(); return; } } QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
能夠看到,當且僅當是 commit()
而且只有一個待寫入操做的時候才能直接執行到 writeToDiskRunnable.run()
,不然都會執行到 QueuedWork
的 queue()
方法,這個 QueuedWork
又是什麼東西?
/** Finishers {@link #addFinisher added} and not yet {@link #removeFinisher removed} */ @GuardedBy("sLock") private static final LinkedList<Runnable> sFinishers = new LinkedList<>(); /** Work queued via {@link #queue} */ @GuardedBy("sLock") private static final LinkedList<Runnable> sWork = new LinkedList<>(); /** * Internal utility class to keep track of process-global work that's outstanding and hasn't been * finished yet. * * New work will be {@link #queue queued}. * * It is possible to add 'finisher'-runnables that are {@link #waitToFinish guaranteed to be run}. * This is used to make sure the work has been finished. * * This was created for writing SharedPreference edits out asynchronously so we'd have a mechanism * to wait for the writes in Activity.onPause and similar places, but we may use this mechanism for * other things in the future. * * The queued asynchronous work is performed on a separate, dedicated thread. * * @hide */ public class QueuedWork { /** * Add a finisher-runnable to wait for {@link #queue asynchronously processed work}. * * Used by SharedPreferences$Editor#startCommit(). * * Note that this doesn't actually start it running. This is just a scratch set for callers * doing async work to keep updated with what's in-flight. In the common case, caller code * (e.g. SharedPreferences) will pretty quickly call remove() after an add(). The only time * these Runnables are run is from {@link #waitToFinish}. * * @param finisher The runnable to add as finisher */ public static void addFinisher(Runnable finisher) { synchronized (sLock) { sFinishers.add(finisher); } } /** * Remove a previously {@link #addFinisher added} finisher-runnable. * * @param finisher The runnable to remove. */ public static void removeFinisher(Runnable finisher) { synchronized (sLock) { sFinishers.remove(finisher); } } /** * Trigger queued work to be processed immediately. The queued work is processed on a separate * thread asynchronous. While doing that run and process all finishers on this thread. The * finishers can be implemented in a way to check weather the queued work is finished. * * Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive, * after Service command handling, etc. (so async work is never lost) */ public static void waitToFinish() { long startTime = System.currentTimeMillis(); boolean hadMessages = false; Handler handler = getHandler(); synchronized (sLock) { if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) { // Delayed work will be processed at processPendingWork() below handler.removeMessages(QueuedWorkHandler.MSG_RUN); if (DEBUG) { hadMessages = true; Log.d(LOG_TAG, "waiting"); } } // We should not delay any work as this might delay the finishers sCanDelay = false; } StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); try { processPendingWork(); } finally { StrictMode.setThreadPolicy(oldPolicy); } try { while (true) { Runnable finisher; synchronized (sLock) { finisher = sFinishers.poll(); } if (finisher == null) { break; } finisher.run(); } } finally { sCanDelay = true; } synchronized (sLock) { long waitTime = System.currentTimeMillis() - startTime; if (waitTime > 0 || hadMessages) { mWaitTimes.add(Long.valueOf(waitTime).intValue()); mNumWaits++; if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) { mWaitTimes.log(LOG_TAG, "waited: "); } } } } /** * Queue a work-runnable for processing asynchronously. * * @param work The new runnable to process * @param shouldDelay If the message should be delayed */ public static void queue(Runnable work, boolean shouldDelay) { Handler handler = getHandler(); synchronized (sLock) { sWork.add(work); if (shouldDelay && sCanDelay) { handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY); } else { handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN); } } } }
簡單地說,這個 QueuedWork
類裏面有一個專門存放 Runnable
的兩個 LinkedList
對象,他們分別對應未完成的操做 sFinishers
和正在工做的 sWork
。
咱們在 waitToFinish()
方法中,會不斷地去遍歷執行未完成的 Runnable
。咱們根據註釋也知道了這個方法會在 Activity
的 onPause()
和 BroadcastReceiver
的 onReceive()
方法後調用。假設咱們頻繁的調用了 apply()
方法,並緊接着調用了 onPause()
,那麼就可能會發生 onPause()
一直等待 QueuedWork.waitToFinish
執行完成而產生 ANR。也就是說,即便是調用了 apply()
方法去異步提交,也不是徹底安全的。若是 apply()
方法使用不當,也是可能出現 ANR 的。
說了這麼多,咱們固然仍是須要作一個總結。
apply()
沒有返回值而 commit()
返回 boolean
代表修改是否提交成功 ;commit()
是把內容同步提交到硬盤的,而 apply()
先當即把修改提交到內存,而後開啓一個異步的線程提交到硬盤,而且若是提交失敗,你不會收到任何通知。commit()
提交是同步過程,效率會比 apply()
異步提交的速度慢,在不關心提交結果是否成功的狀況下,優先考慮 apply()
方法。apply()
是使用異步線程寫入磁盤,commit()
是同步寫入磁盤。因此咱們在主線程使用的 commit()
的時候,須要考慮是否會出現 ANR 問題。SharedPreferencesImpl
裏面的對象,互斥其餘操做,而當 put
、commit()
和 apply()
操做的時候都會鎖住 Editor
的對象,在這樣的狀況下,效率會下降。基於以上缺點:
SharedPreferences
,也不要把較多數據存儲到同一個 name 對應的 SharedPreferences
中,最好根據規則拆分爲多個 SharedPreferences
文件。SharedPreferences
。SharedPreferences
對象的時候會讀取 SharedPreferences
文件,若是文件沒有讀取完,就執行了 get 和 put 操做,可能會出現須要等待的狀況,所以最好提早獲取 SharedPreferences
對象。edit()
方法都會建立一個新的 EditorImpl
對象,不要頻繁調用 edit()
方法。