目錄介紹
- 01.Sp簡單介紹
- 1.1 Sp作用分析
- 1.2 案例分析思考
- 02.Sp初始化操作
- 2.1 如何擷取sp
- 2.2 SharedPreferencesImpl構造
- 03.edit方法源碼
- 04.put和get方法源碼
- 4.1 put方法源碼
- 4.2 get方法源碼
- 05.commit和apply
- 5.1 commit源碼
- 5.2 apply源碼
- 06.總結分析
好消息
- 部落格筆記大彙總【16年3月到至今】,包括Java基礎及深入知識點,Android技術部落格,Python學習筆記等等,還包括平時開發中遇到的bug彙總,當然也在工作之餘收集了大量的面試題,長期更新維護并且修正,持續完善……開源的檔案是markdown格式的!同時也開源了生活部落格,從12年起,積累共計N篇[近100萬字,陸續搬到網上],轉載請注明出處,謝謝!
- 連結位址: https://github.com/yangchong211/YCBlogs
- 如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起于忽微,量變引起質變!
01.Sp簡單介紹說明
- sp作用說明
- SharedPreferences是Android中比較常用的存儲方法,它可以用來存儲一些比較小的鍵值對集合,并最終會在手機的/data/data/package_name/shared_prefs/目錄下生成一個 xml 檔案存儲資料。
- 分析sp包含那些内容
- 擷取SharedPreferences對象過程中,系統做了什麼?
- getXxx方法做了什麼?
- putXxx方法做了什麼?
- commit/apply方法如何實作同步/異步寫磁盤?
- 分析sp包含那些源碼
- SharedPreferences 接口
- SharedPreferencesImpl 實作類
- QueuedWork 類
1.2.1 edit用法分析
- 代碼如下所示
long startA = System.currentTimeMillis(); for (int i=0 ; i<200 ; i++){ SharedPreferences preferences = this.getSharedPreferences("testA", 0); SharedPreferences.Editor edit = preferences.edit(); edit.putString("yc"+i,"yangchong"+i); edit.commit(); } long endA = System.currentTimeMillis(); long a = endA - startA; Log.i("測試A","----"+a); long startB = System.currentTimeMillis(); SharedPreferences preferencesB = this.getSharedPreferences("testB", 0); SharedPreferences.Editor editB = preferencesB.edit(); for (int i=0 ; i<200 ; i++){ editB.putString("yc"+i,"yangchong"+i); } editB.commit(); long endB = System.currentTimeMillis(); long b = endB - startB; Log.i("測試B","----"+b); long startC = System.currentTimeMillis(); SharedPreferences.Editor editC = null; for (int i=0 ; i<200 ; i++){ SharedPreferences preferencesC = this.getSharedPreferences("testC", 0); if (editC==null){ editC = preferencesC.edit(); } editC.putString("yc"+i,"yangchong"+i); } editC.commit(); long endC = System.currentTimeMillis(); long c = endC - startC; Log.i("測試C","----"+c);
- 然後開始執行操作
- A操作和B操作,在代碼邏輯上應該是一樣的,都是想SP中寫入200次不同字段的資料,差別隻是在于,A操作每次都去擷取新的Editor,而B操作是隻使用一個Eidtor去存儲。兩個操作都分别執行兩次。
- A操作和C操作,在代碼邏輯上應該是一樣的,都是想SP中寫入200次不同字段的資料,差別隻是在于,A操作每次都去擷取新的Editor,而C操作是隻使用一個Editor去存儲,并且隻commit一次。兩個操作都分别執行兩次。
- B和C的操作幾乎都是一樣的,唯一不同的是B操作隻是擷取一次preferencesB對象,而C操作則是擷取200次preferencesC操作。
- 然後看一下執行結果
2019-08-30 15:08:16.982 3659-3659/com.cheoo.app I/測試A: ----105 2019-08-30 15:08:17.035 3659-3659/com.cheoo.app I/測試B: ----52 2019-08-30 15:08:17.069 3659-3659/com.cheoo.app I/測試C: ----34 2019-08-30 15:08:20.561 3659-3659/com.cheoo.app I/測試A: ----25 2019-08-30 15:08:20.562 3659-3659/com.cheoo.app I/測試B: ----1 2019-08-30 15:08:20.564 3659-3659/com.cheoo.app I/測試C: ----2
- 結果分析
- 通過A和B操作進行比較可知:使用commit()的方式,如果每次都使用sp.edit()方法擷取一個新的Editor的話,建立和修改的執行效率差了非常的大。也就是說,存儲一個從來沒有用過的Key,和修改一個已經存在的Key,在效率上是有差别的。
- 通過B和C操作進行比較可知:getSharedPreferences操作一次和多次其實是沒有多大的差別,因為在有緩存,如果存在則從緩存中取。
- 然後看看裡面存儲值
- 其存儲的值并不是按照順序的。
<?xml version='1.0' encoding='utf-8' standalone='yes' ?> <map> <string name="yc110">yangchong110</string> <string name="yc111">yangchong111</string> <string name="yc118">yangchong118</string> <string name="yc119">yangchong119</string> <string name="yc116">yangchong116</string> <string name="yc117">yangchong117</string> <string name="yc114">yangchong114</string> <string name="yc115">yangchong115</string> <string name="yc112">yangchong112</string> <string name="yc113">yangchong113</string> <string name="yc121">yangchong121</string> <string name="yc122">yangchong122</string> <string name="yc120">yangchong120</string> <string name="yc129">yangchong129</string> <string name="yc127">yangchong127</string> <string name="yc128">yangchong128</string> <string name="yc125">yangchong125</string> <string name="yc126">yangchong126</string> <string name="yc123">yangchong123</string> <string name="yc124">yangchong124</string> <string name="yc1">yangchong1</string> <string name="yc109">yangchong109</string> <string name="yc0">yangchong0</string> <string name="yc3">yangchong3</string> </map>
1.2.2 commit和apply
-
long startA = System.currentTimeMillis(); for (int i=0 ; i<200 ; i++){ SharedPreferences preferences = activity.getSharedPreferences("testA", 0); SharedPreferences.Editor edit = preferences.edit(); edit.putString("yc"+i,"yangchong"+i); edit.apply(); } long endA = System.currentTimeMillis(); long a = endA - startA; Log.i("測試A","----"+a); long startB = System.currentTimeMillis(); SharedPreferences preferencesB = activity.getSharedPreferences("testB", 0); SharedPreferences.Editor editB = preferencesB.edit(); for (int i=0 ; i<200 ; i++){ editB.putString("yc"+i,"yangchong"+i); } editB.apply(); long endB = System.currentTimeMillis(); long b = endB - startB; Log.i("測試B","----"+b); long startC = System.currentTimeMillis(); SharedPreferences.Editor editC = null; for (int i=0 ; i<200 ; i++){ SharedPreferences preferencesC = activity.getSharedPreferences("testC", 0); if (editC==null){ editC = preferencesC.edit(); } editC.putString("yc"+i,"yangchong"+i); } editC.apply(); long endC = System.currentTimeMillis(); long c = endC - startC; Log.i("測試C","----"+c);
-
2019-08-30 15:17:07.341 5522-5522/com.cheoo.app I/測試A: ----54 2019-08-30 15:17:07.346 5522-5522/com.cheoo.app I/測試B: ----5 2019-08-30 15:17:07.352 5522-5522/com.cheoo.app I/測試C: ----6 2019-08-30 15:17:10.541 5522-5522/com.cheoo.app I/測試A: ----32 2019-08-30 15:17:10.542 5522-5522/com.cheoo.app I/測試B: ----1 2019-08-30 15:17:10.543 5522-5522/com.cheoo.app I/測試C: ----1
- 得出結論
- 從執行結果可以發現,使用apply因為是異步操作,基本上是不耗費時間的,效率上都是OK的。從這個結論上來看,apply影響效率的地方,在sp.edit()方法。
- 可以看出多次執行edit方法還是很影響效率的。
- 在edit()中是有synchronized這個同步鎖來保證線程安全的,縱觀EditorImpl.java的實作,可以看到大部分操作都是有同步鎖的,但是隻鎖了(this),也就是隻對目前對象有效,而edit()方法是每次都會去重新new一個EditorImpl()這個Eidtor接口的實作類。是以效率就應該是被這裡影響到了。
@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(); }
1.2.3 給出的建議
- edit()是有效率影響的,是以不要在循環中去調用吃方法,最好将edit()方法擷取的Editor對象方在循環之外,在循環中共用同一個Editor()對象進行操作。
- commit()的時候,「new-key」和「update-key」的效率是有差别的,但是有傳回結果。
- apply()是異步操作,對效率的影響,基本上是ms級的,可以忽略不記。
- 首先看ContextWrapper源碼
@Override public SharedPreferences getSharedPreferences(String name, int mode) { return mBase.getSharedPreferences(name, mode); }
- 然後看一下ContextImpl類
@Override 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) { // 建立一個對應路徑 /data/data/packageName/name 的 File 對象 file = getSharedPreferencesPath(name); mSharedPrefsPaths.put(name, file); } } // 這裡調用了 getSharedPreferences(File file, int mode) 方法 return getSharedPreferences(file, mode); }
- 然後接着看一下getSharedPreferences(file, mode)方法源碼
@Override public SharedPreferences getSharedPreferences(File file, int mode) { SharedPreferencesImpl sp; // 這裡使用了 synchronized 關鍵字,確定了 SharedPreferences 對象的構造是線程安全的 synchronized (ContextImpl.class) { // 擷取SharedPreferences 對象的緩存,并複制給 cache final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked(); // 以參數 file 作為 key,擷取緩存對象 sp = cache.get(file); if (sp == null) { // 如果緩存中不存在 SharedPreferences 對象 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"); } } // 構造一個 SharedPreferencesImpl 對象 sp = new SharedPreferencesImpl(file, mode); // 放入緩存 cache 中,友善下次直接從緩存中擷取 cache.put(file, sp); // 傳回新構造的 SharedPreferencesImpl 對象 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. // 如果由其他程序修改了這個 SharedPreferences 檔案,我們将會重新加載它 sp.startReloadIfChangedUnexpectedly(); } // 程式走到這裡,說明命中了緩存,SharedPreferences 已經建立,直接傳回 return sp; }
- 這段源碼的流程還是清晰易懂的,注釋已經說得很明白,這裡我們總結一下這個方法的要點:
- 緩存未命中, 才構造SharedPreferences對象,也就是說,多次調用getSharedPreferences方法并不會對性能造成多大影響,因為又緩存機制。
- SharedPreferences對象的建立過程是線程安全的,因為使用了synchronize關鍵字。
- 如果命中了緩存,并且參數mode使用了Context.MODE_MULTI_PROCESS,那麼将會調用sp.startReloadIfChangedUnexpectedly()方法,在startReloadIfChangedUnexpectedly方法中,會判斷是否由其他程序修改過這個檔案,如果有,會重新從磁盤中讀取檔案加載資料。
- 看SharedPreferencesImpl的構造方法,源碼如下所示
- 将傳進來的參數file以及mode分别儲存在mFile以及mMode中
- 建立一個.bak備份檔案,當使用者寫入失敗的時候會根據這個備份檔案進行恢複工作
- 将存放鍵值對的mMap初始化為null
- 調用startLoadFromDisk()方法加載資料
// SharedPreferencesImpl.java // 構造方法 SharedPreferencesImpl(File file, int mode) { mFile = file; // 建立災備檔案,命名為prefsFile.getPath() + ".bak" mBackupFile = makeBackupFile(file); mMode = mode; // mLoaded代表是否已經加載完資料 mLoaded = false; // 解析 xml 檔案得到的鍵值對就存放在mMap中 mMap = null; // 顧名思義,這個方法用于加載 mFile 這個磁盤上的 xml 檔案 startLoadFromDisk(); } // 建立災備檔案,用于當使用者寫入失敗的時候恢複資料 private static File makeBackupFile(File prefsFile) { return new File(prefsFile.getPath() + ".bak"); }
- 然後看一下調用startLoadFromDisk()方法加載資料
// SharedPreferencesImpl.java private void startLoadFromDisk() { synchronized (this) { mLoaded = false; } //注意:這裡我們可以看出,SharedPreferences 是通過開啟一個線程來異步加載資料的 new Thread("SharedPreferencesImpl-load") { public void run() { // 這個方法才是真正負責從磁盤上讀取 xml 檔案資料 loadFromDisk(); } }.start(); } private void loadFromDisk() { synchronized (SharedPreferencesImpl.this) { // 如果正在加載資料,直接傳回 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 map = null; StructStat stat = null; try { // 擷取檔案資訊,包括檔案修改時間,檔案大小等 stat = Os.stat(mFile.getPath()); if (mFile.canRead()) { BufferedInputStream str = null; try { // 讀取資料并且将資料解析為jia str = new BufferedInputStream( new FileInputStream(mFile), *); map = XmlUtils.readMapXml(str); } catch (XmlPullParserException | IOException e) { Log.w(TAG, "getSharedPreferences", e); } finally { IoUtils.closeQuietly(str); } } } catch (ErrnoException e) { /* ignore */ } synchronized (SharedPreferencesImpl.this) { // 加載資料成功,設定 mLoaded 為 true mLoaded = true; if (map != null) { // 将解析得到的鍵值對資料指派給 mMap mMap = map; // 将檔案的修改時間戳儲存到 mStatTimestamp 中 mStatTimestamp = stat.st_mtime; // 将檔案的大小儲存到 mStatSize 中 mStatSize = stat.st_size; } else { mMap = new HashMap<>(); } // 通知喚醒所有等待的線程 notifyAll(); } }
- 對startLoadFromDisk()方法進行了分析,有分析我們可以得到以下幾點總結:
- 如果有備份檔案,直接使用備份檔案進行復原
- 第一次調用getSharedPreferences方法的時候,會從磁盤中加載資料,而資料的加載時通過開啟一個子線程調用loadFromDisk方法進行異步讀取的
- 将解析得到的鍵值對資料儲存在mMap中
- 将檔案的修改時間戳以及大小分别儲存在mStatTimestamp以及mStatSize中(儲存這兩個值有什麼用呢?我們在分析getSharedPreferences方法時說過,如果有其他程序修改了檔案,并且mode為MODE_MULTI_PROCESS,将會判斷重新加載檔案。如何判斷檔案是否被其他程序修改過,沒錯,根據檔案修改時間以及檔案大小即可知道)
- 調用notifyAll()方法通知喚醒其他等待線程,資料已經加載完畢
- 源碼方法如下所示
@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(); }
- 就以putString為例分析源碼。通過sharedPreferences.edit()方法傳回的SharedPreferences.Editor,所有我們對SharedPreferences的寫操作都是基于這個Editor類的。在 Android 系統中,Editor是一個接口類,它的具體實作類是EditorImpl:
public final class EditorImpl implements Editor { // putXxx/remove/clear等寫操作方法都不是直接操作 mMap 的,而是将所有 // 的寫操作先記錄在 mModified 中,等到 commit/apply 方法被調用,才會将 // 所有寫操作同步到 記憶體中的 mMap 以及磁盤中 private final Map<String, Object> mModified = Maps.newHashMap(); // private boolean mClear = false; public Editor putString(String key, @Nullable String value) { synchronized (this) { mModified.put(key, value); return this; } } ...... 其他方法 ...... }
- 從EditorImpl類的源碼我們可以得出以下總結:
- SharedPreferences的寫操作是線程安全的,因為使用了synchronize關鍵字
- 對鍵值對資料的增删記錄儲存在mModified中,而并不是直接對SharedPreferences.mMap進行操作(mModified會在commit/apply方法中起到同步記憶體SharedPreferences.mMap以及磁盤資料的作用)
- 就以getString為例分析源碼
@Nullable public String getString(String key, @Nullable String defValue) { // synchronize 關鍵字用于保證 getString 方法是線程安全的 synchronized (this) { // 方法 awaitLoadedLocked() 用于確定加載完資料并儲存到 mMap 中才進行資料讀取 awaitLoadedLocked(); // 根據 key 從 mMap中擷取 value String v = (String)mMap.get(key); // 如果 value 不為 null,傳回 value,如果為 null,傳回預設值 return v != null ? v : defValue; } } 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(); } // 前面我們說過,mLoaded 代表資料是否已經加載完畢 while (!mLoaded) { try { // 等待資料加載完成之後才傳回繼續執行代碼 wait(); } catch (InterruptedException unused) { } } }
- getString方法代碼很簡單,其他的例如getInt,getFloat方法也是一樣的原理,直接對這個疑問進行總結:
- getXxx方法是線程安全的,因為使用了synchronize關鍵字
- getXxx方法是直接操作記憶體的,直接從記憶體中的mMap中根據傳入的key讀取value
- getXxx方法有可能會卡在awaitLoadedLocked方法,進而導緻線程阻塞等待(什麼時候會出現這種阻塞現象呢?前面我們分析過,第一次調用getSharedPreferences方法時,會建立一個線程去異步加載資料,那麼假如在調用完getSharedPreferences方法之後立即調用getXxx方法,此時的mLoaded很有可能為false,這就會導緻awaiteLoadedLocked方法阻塞等待,直到loadFromDisk方法加載完資料并且調用notifyAll來喚醒所有等待線程)
- commit()方法分析
public boolean commit() { // 前面我們分析 putXxx 的時候說過,寫操作的記錄是存放在 mModified 中的 // 在這裡,commitToMemory() 方法就負責将 mModified 儲存的寫記錄同步到記憶體中的 mMap 中 // 并且傳回一個 MemoryCommitResult 對象 MemoryCommitResult mcr = commitToMemory(); // enqueueDiskWrite 方法負責将資料落地到磁盤上 SharedPreferencesImpl.this.enqueueDiskWrite( mcr, null /* sync write on this thread okay */); try { // 同步等待資料落地磁盤工作完成才傳回 mcr.writtenToDiskLatch.await(); } catch (InterruptedException e) { return false; } // 通知觀察者 notifyListeners(mcr); return mcr.writeToDiskResult; }
- commit()方法的主體結構很清晰簡單:
- 首先将寫操作記錄同步到記憶體的SharedPreferences.mMap中(将mModified同步到mMap)
- 然後調用enqueueDiskWrite方法将資料寫入到磁盤上
- 同步等待寫磁盤操作完成(這就是為什麼commit()方法會同步阻塞等待的原因)
- 通知監聽者(可以通過registerOnSharedPreferenceChangeListener方法注冊監聽)
- 最後傳回執行結果:true or false
- commit()方法的主體結構很清晰簡單:
- 接着來看一下它調用的commitToMemory()方法:
private MemoryCommitResult commitToMemory() { MemoryCommitResult mcr = new MemoryCommitResult(); synchronized (SharedPreferencesImpl.this) { // 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); } // 将 mMap 指派給 mcr.mapToWriteToDisk,mcr.mapToWriteToDisk 指向的就是最終寫入磁盤的資料 mcr.mapToWriteToDisk = mMap; // mDiskWritesInFlight 代表的是“此時需要将資料寫入磁盤,但還未處理或未處理完成的次數” // 将 mDiskWritesInFlight 自增1(這裡是唯一會增加 mDiskWritesInFlight 的地方) mDiskWritesInFlight++; boolean hasListeners = mListeners.size() > 0; if (hasListeners) { mcr.keysModified = new ArrayList<String>(); mcr.listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet()); } synchronized (this) { // 隻有調用clear()方法,mClear才為 true if (mClear) { if (!mMap.isEmpty()) { mcr.changesMade = true; // 當 mClear 為 true,清空 mMap mMap.clear(); } mClear = false; } // 周遊 mModified for (Map.Entry<String, Object> e : mModified.entrySet()) { String k = e.getKey(); // 擷取 key Object v = e.getValue(); // 擷取 value // 當 value 的值是 "this" 或者 null,将對應 key 的鍵值對資料從 mMap 中移除 if (v == this || v == null) { if (!mMap.containsKey(k)) { continue; } mMap.remove(k); } else { // 否則,更新或者添加鍵值對資料 if (mMap.containsKey(k)) { Object existingValue = mMap.get(k); if (existingValue != null && existingValue.equals(v)) { continue; } } mMap.put(k, v); } mcr.changesMade = true; if (hasListeners) { mcr.keysModified.add(k); } } // 将 mModified 同步到 mMap 之後,清空 mModified 曆史記錄 mModified.clear(); } } return mcr; }
- commitToMemory()方法主要做了這幾件事:
- mDiskWritesInFlight自增1(mDiskWritesInFlight代表“此時需要将資料寫入磁盤,但還未處理或未處理完成的次數”,提示,整個SharedPreferences的源碼中,唯獨在commitToMemory()方法中“有且僅有”一處代碼會對mDiskWritesInFlight進行增加,其他地方都是減)
- 将mcr.mapToWriteToDisk指向mMap,mcr.mapToWriteToDisk就是最終需要寫入磁盤的資料
- 判斷mClear的值,如果是true,清空mMap(調用clear()方法,會設定mClear為true)
- 同步mModified資料到mMap中,然後清空mModified最後傳回一個MemoryCommitResult對象,這個對象的mapToWriteToDisk參數指向了最終需要寫入磁盤的mMap
- commitToMemory()方法主要做了這幾件事:
- 對調用的enqueueDiskWrite方法進行分析:
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) { // 建立一個 Runnable 對象,該對象負責寫磁盤操作 final Runnable writeToDiskRunnable = new Runnable() { public void run() { synchronized (mWritingToDiskLock) { // 顧名思義了,這就是最終通過檔案操作将資料寫入磁盤的方法了 writeToFile(mcr); } synchronized (SharedPreferencesImpl.this) { // 寫入磁盤後,将 mDiskWritesInFlight 自減1,代表寫磁盤的需求減少一個 mDiskWritesInFlight--; } if (postWriteRunnable != null) { // 執行 postWriteRunnable(提示,在 apply 中,postWriteRunnable 才不為 null) postWriteRunnable.run(); } } }; // 如果傳進的參數 postWriteRunnable 為 null,那麼 isFromSyncCommit 為 true // 溫馨提示:從上面的 commit() 方法源碼中,可以看出調用 commit() 方法傳入的 postWriteRunnable 為 null final boolean isFromSyncCommit = (postWriteRunnable == null); // Typical #commit() path with fewer allocations, doing a write on the current thread. if (isFromSyncCommit) { boolean wasEmpty = false; synchronized (SharedPreferencesImpl.this) { // 如果此時隻有一個 commit 請求(注意,是 commit 請求,而不是 apply )未處理,那麼 wasEmpty 為 true wasEmpty = mDiskWritesInFlight == 1; } if (wasEmpty) { // 當隻有一個 commit 請求未處理,那麼無需開啟線程進行處理,直接在本線程執行 writeToDiskRunnable 即可 writeToDiskRunnable.run(); return; } } // 将 writeToDiskRunnable 方法線程池中執行 // 程式執行到這裡,有兩種可能: // 1. 調用的是 commit() 方法,并且目前隻有一個 commit 請求未處理 // 2. 調用的是 apply() 方法 QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable); } private void writeToFile(MemoryCommitResult mcr) { // Rename the current file so it may be used as a backup during the next read if (mFile.exists()) { if (!mcr.changesMade) { // If the file already exists, but no changes were // made to the underlying map, it's wasteful to // re-write the file. Return as if we wrote it // out. mcr.setDiskWriteResult(true); return; } if (!mBackupFile.exists()) { if (!mFile.renameTo(mBackupFile)) { Log.e(TAG, "Couldn't rename file " + mFile + " to backup file " + mBackupFile); mcr.setDiskWriteResult(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 (str == null) { mcr.setDiskWriteResult(false); return; } XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str); FileUtils.sync(str); str.close(); ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0); try { final StructStat stat = Libcore.os.stat(mFile.getPath()); synchronized (this) { mStatTimestamp = stat.st_mtime; mStatSize = stat.st_size; } } catch (ErrnoException e) { // Do nothing } // Writing was successful, delete the backup file if there is one. mBackupFile.delete(); mcr.setDiskWriteResult(true); 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); }
- writeToFile這個方法大緻分為三個過程:
- 先把已存在的老的 SP 檔案重命名(加“.bak”字尾),然後删除老的 SP 檔案,這相當于做了備份(災備)
- 向mFile中一次性寫入所有鍵值對資料,即mcr.mapToWriteToDisk(這就是commitToMemory所說的儲存了所有鍵值對資料的字段) 一次性寫入到磁盤。
- 如果寫入成功則删除備份(災備)檔案,同時記錄了這次同步的時間如果往磁盤寫入資料失敗,則删除這個半成品的 SP 檔案
- writeToFile這個方法大緻分為三個過程:
- apply()方法分析
public void apply() { // 将 mModified 儲存的寫記錄同步到記憶體中的 mMap 中,并且傳回一個 MemoryCommitResult 對象 final MemoryCommitResult mcr = commitToMemory(); final Runnable awaitCommit = new Runnable() { public void run() { try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException ignored) { } } }; QueuedWork.add(awaitCommit); Runnable postWriteRunnable = new Runnable() { public void run() { awaitCommit.run(); QueuedWork.remove(awaitCommit); } }; // 将資料落地到磁盤上,注意,傳入的 postWriteRunnable 參數不為 null,是以在 // enqueueDiskWrite 方法中會開啟子線程異步将資料寫入到磁盤中 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()方法:
- commitToMemory()方法将mModified中記錄的寫操作同步回寫到記憶體 SharedPreferences.mMap 中。此時, 任何的getXxx方法都可以擷取到最新資料了
- 通過enqueueDiskWrite方法調用writeToFile将方法将所有資料異步寫入到磁盤中
- 總結一下apply()方法:
- SharedPreferences是線程安全的,它的内部實作使用了大量synchronized關鍵字
- SharedPreferences不是程序安全的
- 第一次調用getSharedPreferences會加載磁盤 xml 檔案(這個加載過程是異步的,通過new Thread來執行,是以并不會在構造SharedPreferences的時候阻塞線程,但是會阻塞getXxx/putXxx/remove/clear等調用),但後續調用getSharedPreferences會從記憶體緩存中擷取。如果第一次調用getSharedPreferences時還沒從磁盤加載完畢就馬上調用getXxx/putXxx,那麼getXxx/putXxx操作會阻塞,直到從磁盤加載資料完成後才傳回
- 所有的getXxx都是從記憶體中取的資料,資料來源于SharedPreferences.mMap
- apply同步回寫(commitToMemory())記憶體SharedPreferences.mMap,然後把異步回寫磁盤的任務放到一個單線程的線程池隊列中等待排程。apply不需要等待寫入磁盤完成,而是馬上傳回
- commit同步回寫(commitToMemory())記憶體SharedPreferences.mMap,然後如果mDiskWritesInFlight(此時需要将資料寫入磁盤,但還未處理或未處理完成的次數)的值等于1,那麼直接在調用commit的線程執行回寫磁盤的操作,否則把異步回寫磁盤的任務放到一個單線程的線程池隊列中等待排程。commit會阻塞調用線程,知道寫入磁盤完成才傳回
- MODE_MULTI_PROCESS是在每次getSharedPreferences時檢查磁盤上配置檔案上次修改時間和檔案大小,一旦所有修改則會重新從磁盤加載檔案,是以并不能保證多程序資料的實時同步
- 從 Android N 開始,,不支援MODE_WORLD_READABLE & MODE_WORLD_WRITEABLE。一旦指定, 直接抛異常
其他介紹
01.關于部落格彙總連結
02.關于我的部落格
- github: https://github.com/yangchong211
- 知乎: https://www.zhihu.com/people/yczbj/activities
- 簡書: http://www.jianshu.com/u/b7b2c6ed9284
- csdn: http://my.csdn.net/m0_37700275
- 喜馬拉雅聽書: http://www.ximalaya.com/zhubo/71989305/
- 開源中國: https://my.oschina.net/zbj1618/blog
- 泡在網上的日子: http://www.jcodecraeer.com/member/content_list.php?channelid=1
- 郵箱:[email protected]
- 阿裡雲部落格: https://yq.aliyun.com/users/article?spm=5176.100- 239.headeruserinfo.3.dT4bcV
- segmentfault頭條: https://segmentfault.com/u/xiangjianyu/articles
- 掘金: https://juejin.im/user/5939433efe88c2006afa0c6e