Sp效率分析和理解

目錄介紹

  • 01.Sp簡單介紹php

    • 1.1 Sp做用分析
    • 1.2 案例分析思考
  • 02.Sp初始化操做java

    • 2.1 如何獲取sp
    • 2.2 SharedPreferencesImpl構造
  • 03.edit方法源碼
  • 04.put和get方法源碼android

    • 4.1 put方法源碼
    • 4.2 get方法源碼
  • 05.commit和applygit

    • 5.1 commit源碼
    • 5.2 apply源碼
  • 06.總結分析

好消息

  • 博客筆記大彙總【16年3月到至今】,包括Java基礎及深刻知識點,Android技術博客,Python學習筆記等等,還包括平時開發中遇到的bug彙總,固然也在工做之餘收集了大量的面試題,長期更新維護而且修正,持續完善……開源的文件是markdown格式的!同時也開源了生活博客,從12年起,積累共計N篇[近100萬字,陸續搬到網上],轉載請註明出處,謝謝!
  • 連接地址:https://github.com/yangchong2...
  • 若是以爲好,能夠star一下,謝謝!固然也歡迎提出建議,萬事起於忽微,量變引發質變!

01.Sp簡單介紹說明

1.1 Sp做用分析

  • sp做用說明github

    • SharedPreferences是Android中比較經常使用的存儲方法,它能夠用來存儲一些比較小的鍵值對集合,並最終會在手機的/data/data/package_name/shared_prefs/目錄下生成一個 xml 文件存儲數據。
  • 分析sp包含那些內容面試

    • 獲取SharedPreferences對象過程當中,系統作了什麼?
    • getXxx方法作了什麼?
    • putXxx方法作了什麼?
    • commit/apply方法如何實現同步/異步寫磁盤?
  • 分析sp包含那些源碼segmentfault

    • SharedPreferences 接口
    • SharedPreferencesImpl 實現類
    • QueuedWork 類

1.2 案例分析思考

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操做。
  • 而後看一下執行結果markdown

    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級的,能夠忽略不記。

02.Sp初始化操做

2.1 如何獲取sp

  • 首先看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 "
}
            }

            // 構造一個 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方法中,會判斷是否由其餘進程修改過這個文件,若是有,會從新從磁盤中讀取文件加載數據。

2.2 SharedPreferencesImpl構造

  • 看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()方法通知喚醒其餘等待線程,數據已經加載完畢

03.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();
    }

04.put和get方法源碼

4.1 put方法源碼

  • 就以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以及磁盤數據的做用)

4.2 get方法源碼

  • 就以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來喚醒全部等待線程)

05.commit和apply

5.1 commit源碼

  • 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
  • 接着來看一下它調用的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
  • 對調用的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
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 文件

5.2 apply源碼

  • 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將方法將全部數據異步寫入到磁盤中

06.總結分析

  • 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.關於個人博客

相關文章
相關標籤/搜索