Sp效率分析和理解

楊充發表於2019-08-30

目錄介紹

  • 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萬字,陸續搬到網上],轉載請註明出處,謝謝!
  • 連結地址:github.com/yangchong21…
  • 如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起於忽微,量變引起質變!

01.Sp簡單介紹說明

1.1 Sp作用分析

  • sp作用說明
    • SharedPreferences是Android中比較常用的儲存方法,它可以用來儲存一些比較小的鍵值對集合,並最終會在手機的/data/data/package_name/shared_prefs/目錄下生成一個 xml 檔案儲存資料。
  • 分析sp包含那些內容
    • 獲取SharedPreferences物件過程中,系統做了什麼?
    • getXxx方法做了什麼?
    • putXxx方法做了什麼?
    • commit/apply方法如何實現同步/非同步寫磁碟?
  • 分析sp包含那些原始碼
    • 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操作。
  • 然後看一下執行結果
    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 "
                                + "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方法中,會判斷是否由其他程式修改過這個檔案,如果有,會重新從磁碟中讀取檔案載入資料。

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
                            + " 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 檔案

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.關於我的部落格

相關文章