Android 初代 K-V 存儲框架 SharedPreferences骇笔,舊時代的余暉省店?

前言

大家好,我是小彭笨触。

SharedPreferences 是 Android 平臺上輕量級的 K-V 存儲框架懦傍,亦是初代 K-V 存儲框架,至今被很多應用沿用芦劣。

有的小伙伴會說粗俱,SharedPreferences 是舊時代的產(chǎn)物,現(xiàn)在已經(jīng)有 DataStore 或 MMKV 等新時代的 K-V 框架虚吟,沒有學習意義寸认。但我認為,雖然 SharedPreference 這個方案已經(jīng)過時串慰,但是并不意味著 SharedPreference 中使用的技術過時废麻。做技術要知其然,更要知其所以然模庐,而不是人云亦云烛愧,如果要你解釋為什么 SharedPreferences 會過時,你能說到什么程度掂碱?

不知道你最近有沒有讀到一本在技術圈非沉耍火爆的一本新書 《安卓傳奇 · Android 締造團隊回憶錄》,其中就講了很多 Android 架構演進中設計者的思考疼燥。如果你平時也有從設計者的角度思考過 “為什么”沧卢,那么很多內(nèi)容會覺得想到一塊去了,反之就會覺得無感醉者。


—— 圖片引用自電商平臺

今天但狭,我們就來分析 SharedPreference 源碼披诗,在過程中依然可以學習到非常豐富的設計技巧。在后續(xù)的文章中立磁,我們會繼續(xù)分析其他 K-V 存儲框架呈队,請關注。

本文源碼分析基于 Android 10(API 31)唱歧,并關聯(lián)分析部分 Android 7.1(API 25)宪摧。


思維導圖:


1. 實現(xiàn) K-V 框架應該思考什么問題?

在閱讀 SharedPreference 的源碼之前颅崩,我們先思考一個 K-V 框架應該考慮哪些問題几于?

  • 問題 1 - 線程安全: 由于程序一般會在多線程環(huán)境中執(zhí)行,因此框架有必要保證多線程并發(fā)安全沿后,并且優(yōu)化并發(fā)效率沿彭;

  • 問題 2 - 內(nèi)存緩存: 由于磁盤 IO 操作是耗時操作,因此框架有必要在業(yè)務層和磁盤文件之間增加一層內(nèi)存緩存尖滚;

  • 問題 3 - 事務: 由于磁盤 IO 操作是耗時操作喉刘,因此框架有必要將支持多次磁盤 IO 操作聚合為一次磁盤寫回事務,減少訪問磁盤次數(shù)熔掺;

  • 問題 4 - 事務串行化: 由于程序可能由多個線程發(fā)起寫回事務饱搏,因此框架有必要保證事務之間的事務串行化,避免先執(zhí)行的事務覆蓋后執(zhí)行的事務置逻;

  • 問題 5 - 異步寫回: 由于磁盤 IO 是耗時操作推沸,因此框架有必要支持后臺線程異步寫回;

  • 問題 6 - 增量更新: 由于磁盤文件內(nèi)容可能很大券坞,因此修改 K-V 時有必要支持局部修改鬓催,而不是全量覆蓋修改;

  • 問題 7 - 變更回調(diào): 由于業(yè)務層可能有監(jiān)聽 K-V 變更的需求恨锚,因此框架有必要支持變更回調(diào)監(jiān)聽宇驾,并且防止出現(xiàn)內(nèi)存泄漏;

  • 問題 8 - 多進程: 由于程序可能有多進程需求猴伶,那么框架如何保證多進程數(shù)據(jù)同步课舍?

  • 問題 9 - 可用性: 由于程序運行中存在不可控的異常和 Crash,因此框架有必要盡可能保證系統(tǒng)可用性他挎,盡量保證系統(tǒng)在遇到異常后的數(shù)據(jù)完整性筝尾;

  • 問題 10 - 高效性: 性能永遠是要考慮的問題,解析办桨、讀取筹淫、寫入和序列化的性能如何提高和權衡;

  • 問題 11 - 安全性: 如果程序需要存儲敏感數(shù)據(jù)呢撞,如何保證數(shù)據(jù)完整性和保密性损姜;

  • 問題 12 - 數(shù)據(jù)遷移: 如果項目中存在舊框架饰剥,如何將數(shù)據(jù)從舊框架遷移至新框架,并且保證可靠性摧阅;

  • 問題 13 - 研發(fā)體驗: 是否模板代碼冗長汰蓉,是否容易出錯。

提出這么多問題后:

你覺得學習 SharedPreferences 有沒有價值呢逸尖?

如果讓你自己寫一個 K-V 框架古沥,你會如何解決這些問題呢瘸右?

新時代的 MMKV 和 DataStore 框架是否良好處理了這些問題娇跟?


2. 從 Sample 開始

SharedPreferences 采用 XML 文件格式持久化鍵值對數(shù)據(jù),文件的存儲位置位于應用沙盒的內(nèi)部存儲 /data/data/<packageName>/shared_prefs/ 位置太颤,每個 XML 文件對應于一個 SharedPreferences 對象苞俘。

在 Activity、Context 和 PreferenceManager 中都存在獲取 SharedPreferences 對象的 API龄章,它們最終都會走到 ContextImpl 中:

ContextImpl.java

class ContextImpl extends Context {

    // 獲取 SharedPreferences 對象
    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        // 后文詳細分析...
    }
}

示例代碼

SharedPreferences sp = getSharedPreferences("prefs", Context.MODE_PRIVATE);

// 創(chuàng)建事務
Editor editor = sp.edit();
editor.putString("name", "XIAO PENG");
// 同步提交事務
boolean result = editor.commit(); 
// 異步提交事務
// editor.apply()

// 讀取數(shù)據(jù)
String blog = sp.getString("name", "PENG");

prefs.xml 文件內(nèi)容

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>    
    <string name="name">XIAO PENG</string>
</map>

3. SharedPreferences 的內(nèi)存緩存

由于磁盤 IO 操作是耗時操作吃谣,如果每一次訪問 SharedPreferences 都執(zhí)行一次 IO 操作就顯得沒有必要,所以 SharedPreferences 會在業(yè)務層和磁盤之間增加一層內(nèi)存緩存做裙。在 ContextImpl 類中岗憋,不僅支持獲取 SharedPreferencesImpl 對象,還負責支持 SharedPreferencesImpl 對象的內(nèi)存緩存锚贱。

ContextImpl 中的內(nèi)存緩存邏輯是相對簡單的:

  • 步驟1:通過文件名 name 映射文件對應的 File 對象仔戈;
  • 步驟 2:通過 File 對象映射文件對應的 SharedPreferencesImpl 對象。

兩個映射表:

  • mSharedPrefsPaths: 緩存 “文件名 to 文件對象” 的映射拧廊;
  • sSharedPrefsCache: 這是一個二級映射表监徘,第一級是包名到 Map 的映射,第二級是緩存 “文件對象 to SP 對象” 的映射吧碾。每個 XML 文件在內(nèi)存中只會關聯(lián)一個全局唯一的 SharedPreferencesImpl 對象

繼續(xù)分析發(fā)現(xiàn): 雖然 ContextImpl 實現(xiàn)了 SharedPreferencesImpl 對象的緩存復用凰盔,但沒有實現(xiàn)緩存淘汰,也沒有提供主動移除緩存的 API倦春。因此户敬,在 APP 運行過程中,隨著訪問的業(yè)務范圍越來越多睁本,這部分 SharedPreferences 內(nèi)存緩存的空間也會逐漸膨脹尿庐。這是一個需要注意的問題。

在 getSharedPreferences() 中還有 MODE_MULTI_PROCESS 標記位的處理:

如果是首次獲取 SharedPreferencesImpl 對象會直接讀取磁盤文件添履,如果是二次獲取 SharedPreferences 對象會復用內(nèi)存緩存屁倔。但如果使用了 MODE_MULTI_PROCESS 多進程模式,則在返回前會檢查磁盤文件相對于最后一次內(nèi)存修改是否變化暮胧,如果變化則說明被其他進程修改锐借,需要重新讀取磁盤文件问麸,以實現(xiàn)多進程下的 “數(shù)據(jù)同步”。

但是這種同步是非常弱的钞翔,因為每個進程本身對磁盤文件的寫回是非實時的严卖,再加上如果業(yè)務層緩存了 getSharedPreferences(…) 返回的對象,更感知不到最新的變化布轿。所以嚴格來說哮笆,SharedPreferences 是不支持多進程的,官方也明確表示不要將 SharedPreferences 用于多進程環(huán)境汰扭。

SharedPreferences 內(nèi)存緩存示意圖

流程圖

ContextImpl.java

class ContextImpl extends Context {

    // SharedPreferences 文件根目錄
    private File mPreferencesDir;

    // <文件名 - 文件>
    @GuardedBy("ContextImpl.class")
    private ArrayMap<String, File> mSharedPrefsPaths;

    // 獲取 SharedPreferences 對象
    @Override
    public SharedPreferences getSharedPreferences(String name, int mode) {
        // 1稠肘、文件名轉文件對象
        File file;
        synchronized (ContextImpl.class) {
            // 1.1 查詢映射表
            if (mSharedPrefsPaths == null) {
                mSharedPrefsPaths = new ArrayMap<>();
            }
            file = mSharedPrefsPaths.get(name);
            // 1.2 緩存未命中,創(chuàng)建 File 對象
            if (file == null) {
                file = getSharedPreferencesPath(name);
                mSharedPrefsPaths.put(name, file);
            }
        }
        // 2萝毛、獲取 SharedPreferences 對象
        return getSharedPreferences(file, mode);
    }
        
    // -> 1.2 緩存未命中项阴,創(chuàng)建 File 對象
    @Override
    public File getSharedPreferencesPath(String name) {
        return makeFilename(getPreferencesDir(), name + ".xml");
    }

    private File getPreferencesDir() {
        synchronized (mSync) {
            // 文件目錄:data/data/[package_name]/shared_prefs/
            if (mPreferencesDir == null) {
                mPreferencesDir = new File(getDataDir(), "shared_prefs");
            }
            return ensurePrivateDirExists(mPreferencesDir);
        }
    }
}

文件對象 to SP 對象:

ContextImpl.java

class ContextImpl extends Context {

    // <包名 - Map>
    // <文件 - SharedPreferencesImpl>
    @GuardedBy("ContextImpl.class")
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

    // -> 2、獲取 SharedPreferences 對象
    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            // 2.1 查詢緩存
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            // 2.2 未命中緩存(首次獲劝拾)
            if (sp == null) {
                // 2.2.1 檢查 mode 標記
                checkMode(mode);
                // 2.2.2 創(chuàng)建 SharedPreferencesImpl 對象
                sp = new SharedPreferencesImpl(file, mode);
                // 2.2.3 緩存
                cache.put(file, sp);
                return sp;
            }
        }
        // 3环揽、命中緩存(二次獲取)
        if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
            getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
            // 判斷當前磁盤文件相對于最后一次內(nèi)存修改是否變化庵佣,如果時則重新加載文件
            sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
    }

    // 根據(jù)包名獲取 <文件 - SharedPreferencesImpl> 映射表
    @GuardedBy("ContextImpl.class")
    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;
    }
    ...
}

4. 讀取和解析磁盤文件

在創(chuàng)建 SharedPreferencesImpl 對象時歉胶,構造函數(shù)會啟動一個子線程去讀取本地磁盤文件,一次性將文件中所有的 XML 數(shù)據(jù)轉化為 Map 散列表巴粪。

需要注意的是: 如果在執(zhí)行 loadFromDisk() 解析文件數(shù)據(jù)的過程中通今,其他線程調(diào)用 getValue 查詢數(shù)據(jù)撰糠,那么就必須等待 mLock 鎖直到解析結束均函。

如果單個 SharedPreferences 的 .xml 文件很大的話,就有可能導致查詢數(shù)據(jù)的線程被長時間被阻塞峡蟋,甚至導致主線程查詢時產(chǎn)生 ANR晶通。這也輔證了 SharedPreferences 只適合保存少量數(shù)據(jù)璃氢,文件過大在解析時會有性能問題。

讀取示意圖

SharedPreferencesImpl.java

// 目標文件
private final File mFile;
// 備份文件(后文詳細分析)
private final File mBackupFile;
// 模式
private final int mMode;
// 鎖
private final Object mLock = new Object();
// 讀取文件標記位
@GuardedBy("mLock")
private boolean mLoaded = false;

SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    mThrowable = null;
    // 讀取并解析文件數(shù)據(jù)
    startLoadFromDisk();
}

private void startLoadFromDisk() {
    synchronized (mLock) {
        mLoaded = false;
    }
    // 子線程
    new Thread("SharedPreferencesImpl-load") {
        public void run() {
            loadFromDisk();
        }
    }.start();
}

// -> 讀取并解析文件數(shù)據(jù)(子線程)
private void loadFromDisk() {
    synchronized (mLock) {
        if (mLoaded) {
            return;
        }
        // 1狮辽、如果存在備份文件一也,則恢復備份數(shù)據(jù)(后文詳細分析)
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }

    Map<String, Object> map = null;
    if (mFile.canRead()) {
        // 2、讀取文件
        BufferedInputStream str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);
        // 3喉脖、將 XML 數(shù)據(jù)解析為 Map 映射表
        map = (Map<String, Object>) XmlUtils.readMapXml(str);
        IoUtils.closeQuietly(str);
    }

    synchronized (mLock) {
        mLoaded = true;

        if (map != null) {
            // 使用解析的映射表
            mMap = map;
        } else {
            // 創(chuàng)建空的映射表
            mMap = new HashMap<>();
        }
        // 4椰苟、喚醒等待 mLock 鎖的線程
        mLock.notifyAll();
    }
}

static File makeBackupFile(File prefsFile) {
    return new File(prefsFile.getPath() + ".bak");
}

查詢數(shù)據(jù)可能會阻塞等待:

SharedPreferencesImpl.java

public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        // 等待 mLoaded 標記位
        awaitLoadedLocked();
        // 查詢數(shù)據(jù)
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

private void awaitLoadedLocked() {
    // “檢查 - 等待” 模式
    while (!mLoaded) {
        try {
            mLock.wait();
        } catch (InterruptedException unused) {
        }
    }
}

5. SharedPreferences 的事務機制

是的,SharedPreferences 也有事務操作树叽。

雖然 ContextImpl 中使用了內(nèi)存緩存舆蝴,但是最終數(shù)據(jù)還是需要執(zhí)行磁盤 IO 持久化到磁盤文件中。如果每一次 “變更操作” 都對應一次磁盤 “寫回操作” 的話,不僅效率低下洁仗,而且沒有必要层皱。

所以 SharedPreferences 會使用 “事務” 機制,將多次變更操作聚合為一個 “事務”赠潦,一次事務最多只會執(zhí)行一次磁盤寫回操作叫胖。雖然 SharedPreferences 源碼中并沒有直接體現(xiàn)出 “Transaction” 之類的命名,但是這就是一種 “事務” 設計她奥,與命名無關瓮增。

5.1 MemoryCommitResult 事務對象

SharedPreferences 的事務操作由 Editor 接口實現(xiàn)。

SharedPreferences 對象本身只保留獲取數(shù)據(jù)的 API哩俭,而變更數(shù)據(jù)的 API 全部集成在 Editor 接口中绷跑。Editor 中會將所有的 putValue 變更操作記錄在 mModified 映射表中,但不會觸發(fā)任何磁盤寫回操作携茂,直到調(diào)用 Editor#commitEditor#apply 方法時你踩,才會一次性以事務的方式發(fā)起磁盤寫回任務诅岩。

比較特殊的是:

  • 在 remove 方法中:會將 this 指針作為特殊的移除標記位讳苦,后續(xù)將通過這個 Value 來判斷是移除鍵值對還是修改 / 新增鍵值對;
  • 在 clear 方法中:只是將 mClear 標記位置位吩谦。

可以看到: 在 Editor#commit 和 Editor#apply 方法中鸳谜,首先都會調(diào)用 Editor#commitToMemery() 收集需要寫回磁盤的數(shù)據(jù),并封裝為一個 MemoryCommitResult 事務對象式廷,隨后就是根據(jù)這個事務對象的信息寫回磁盤咐扭。

SharedPreferencesImpl.java

final class SharedPreferencesImpl implements SharedPreferences {

    // 創(chuàng)建修改器對象
    @Override
    public Editor edit() {
        // 等待磁盤文件加載完成
        synchronized (mLock) {
            awaitLoadedLocked();
        }
        // 創(chuàng)建修改器對象
        return new EditorImpl();
    }

    // 修改器
    // 非靜態(tài)內(nèi)部類(會持有外部類 SharedPreferencesImpl 的引用)
    public final class EditorImpl implements Editor {

        // 鎖對象
        private final Object mEditorLock = new Object();

        // 修改記錄(將以事務方式寫回磁盤)
        @GuardedBy("mEditorLock")
        private final Map<String, Object> mModified = new HashMap<>();

        // 清除全部數(shù)據(jù)的標記位
        @GuardedBy("mEditorLock")
        private boolean mClear = false;

        // 修改 String 類型鍵值對
        @Override
        public Editor putString(String key, @Nullable String value) {
            synchronized (mEditorLock) {
                mModified.put(key, value);
                return this;
            }
        }

        // 修改 int 類型鍵值對
        @Override
        public Editor putInt(String key, int value) {
            synchronized (mEditorLock) {
                mModified.put(key, value);
                return this;
            }
        }

        // 移除鍵值對
        @Override
        public Editor remove(String key) {
            synchronized (mEditorLock) {
                // 將 this 指針作為特殊的移除標記位
                mModified.put(key, this);
                return this;
            }
        }

        // 清空鍵值對
        @Override
        public Editor clear() {
            synchronized (mEditorLock) {
                // 清除全部數(shù)據(jù)的標記位
                mClear = true;
                return this;
            }
        }

        ...

        @Override
        public void apply() {
            // commitToMemory():寫回磁盤的數(shù)據(jù)并封裝事務對象
            MemoryCommitResult mcr = commitToMemory();
            // 同步寫回,下文詳細分析
        }

        @Override
        public boolean commit() {
            // commitToMemory():寫回磁盤的數(shù)據(jù)并封裝事務對象
            final MemoryCommitResult mcr = commitToMemory();
            // 異步寫回滑废,下文詳細分析
        }
    }
}

MemoryCommitResult 事務對象核心的字段只有 2 個:

  • memoryStateGeneration: 當前的內(nèi)存版本(在 writeToFile() 中會過濾低于最新的內(nèi)存版本的無效事務)蝗肪;
  • mapToWriteToDisk: 最終全量覆蓋寫回磁盤的數(shù)據(jù)。

SharedPreferencesImpl.java

private static class MemoryCommitResult {
    // 內(nèi)存版本
    final long memoryStateGeneration;
    // 需要全量覆蓋寫回磁盤的數(shù)據(jù)
    final Map<String, Object> mapToWriteToDisk;
    // 同步計數(shù)器
    final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);

    @GuardedBy("mWritingToDiskLock")
    volatile boolean writeToDiskResult = false;
    boolean wasWritten = false;

    // 后文寫回結束后調(diào)用
    void setDiskWriteResult(boolean wasWritten, boolean result) {
        this.wasWritten = wasWritten;
        // writeToDiskResult 會作為 commit 同步寫回的返回值
        writeToDiskResult = result;
        // 喚醒等待鎖
        writtenToDiskLatch.countDown();
    }
}

5.2 創(chuàng)建 MemoryCommitResult 事務對象

下面蠕趁,我們先來分析創(chuàng)建 Editor#commitToMemery() 中 MemoryCommitResult 事務對象的步驟薛闪,核心步驟分為 3 步:

  • 步驟 1 - 準備映射表

首先,檢查 SharedPreferencesImpl#mDiskWritesInFlight 變量俺陋,如果 mDiskWritesInFlight == 0 則說明不存在并發(fā)寫回的事務豁延,那么 mapToWriteToDisk 就只會直接指向 SharedPreferencesImpl 中的 mMap 映射表。如果存在并發(fā)寫回腊状,則會深拷貝一個新的映射表诱咏。

mDiskWritesInFlight 變量是記錄進行中的寫回事務數(shù)量記錄,每執(zhí)行一次 commitToMemory() 創(chuàng)建事務對象時缴挖,就會將 mDiskWritesInFlight 變量會自增 1袋狞,并在寫回事務結束后 mDiskWritesInFlight 變量會自減 1。

  • 步驟 2 - 合并變更記錄

其次,遍歷 mModified 映射表將所有的變更記錄(新增苟鸯、修改或刪除)合并到 mapToWriteToDisk 中(此時法焰,Editor 中的數(shù)據(jù)已經(jīng)同步到內(nèi)存緩存中)。

這一步中的關鍵點是:如果發(fā)生有效修改倔毙,則會將 SharedPreferencesImpl 對象中的 mCurrentMemoryStateGeneration 最新內(nèi)存版本自增 1埃仪,比最新內(nèi)存版本小的事務會被視為無效事務。

  • 步驟 3 - 創(chuàng)建事務對象

最后陕赃,使用 mapToWriteToDisk 和 mCurrentMemoryStateGeneration 創(chuàng)建 MemoryCommitResult 事務對象卵蛉。

事務示意圖

SharedPreferencesImpl.java

final class SharedPreferencesImpl implements SharedPreferences {

    // 進行中事務計數(shù)(在提交事務是自增 1,在寫回結束時自減 1)
    @GuardedBy("mLock")
    private int mDiskWritesInFlight = 0;

    // 內(nèi)存版本
    @GuardedBy("this")
    private long mCurrentMemoryStateGeneration;

    // 磁盤版本
    @GuardedBy("mWritingToDiskLock")
    private long mDiskStateGeneration;

    // 修改器
    public final class EditorImpl implements Editor {

        // 鎖對象
        private final Object mEditorLock = new Object();

        // 修改記錄(將以事務方式寫回磁盤)
        @GuardedBy("mEditorLock")
        private final Map<String, Object> mModified = new HashMap<>();

        // 清除全部數(shù)據(jù)的標記位
        @GuardedBy("mEditorLock")
        private boolean mClear = false;

        // 獲取需要寫回磁盤的事務
        private MemoryCommitResult commitToMemory() {
            long memoryStateGeneration;
            boolean keysCleared = false;
            List<String> keysModified = null;
            Set<OnSharedPreferenceChangeListener> listeners = null;
            Map<String, Object> mapToWriteToDisk;

            synchronized (SharedPreferencesImpl.this.mLock) {
                // 如果同時存在多個寫回事務么库,則使用深拷貝 
                if (mDiskWritesInFlight > 0) {
                    mMap = new HashMap<String, Object>(mMap);
                }
                // mapToWriteToDisk:需要寫回的數(shù)據(jù)
                mapToWriteToDisk = mMap;
                // mDiskWritesInFlight:進行中事務自增 1
                mDiskWritesInFlight++;

                synchronized (mEditorLock) {
                    // changesMade:標記是否發(fā)生有效修改
                    boolean changesMade = false;

                    // 清除全部鍵值對
                    if (mClear) {
                        // 清除 mapToWriteToDisk 映射表(下面的 mModified 有可能重新增加鍵值對)
                        if (!mapToWriteToDisk.isEmpty()) {
                            changesMade = true;
                            mapToWriteToDisk.clear();
                        }
                        keysCleared = true;
                        mClear = false;
                    }

                    // 將 Editor 中的 mModified 修改記錄合并到 mapToWriteToDisk
                    // mapToWriteToDisk 指向 SharedPreferencesImpl 中的 mMap傻丝,所以內(nèi)存緩存越會被修改
                    for (Map.Entry<String, Object> e : mModified.entrySet()) {
                        String k = e.getKey();
                        Object v = e.getValue();
                        if (v == this /*使用 this 指針作為魔數(shù)*/|| 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);
                        }
                        // 標記發(fā)生有效修改
                        changesMade = true;
                        // 記錄變更的鍵值對
                        if (hasListeners) {
                            keysModified.add(k);
                        }
                    }
                    // 重置修改記錄
                    mModified.clear();
                    // 如果發(fā)生有效修改,內(nèi)存版本自增 1
                    if (changesMade) {
                        mCurrentMemoryStateGeneration++;
                    }
                    // 記錄當前的內(nèi)存版本
                    memoryStateGeneration = mCurrentMemoryStateGeneration;
                }
            }
            return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified, listeners, mapToWriteToDisk);
        }
    }
}

步驟 2 - 合并變更記錄中诉儒,存在一種 “反直覺” 的 clear() 操作:

如果在 Editor 中存在 clear() 操作葡缰,并且 clear 前后都有 putValue 操作,就會出現(xiàn)反常的效果:如以下示例程序忱反,按照直觀的預期效果泛释,最終寫回磁盤的鍵值對應該只有 <age>,但事實上最終 <name> 和 <age> 兩個鍵值對都會被寫回磁盤温算。

出現(xiàn)這個 “現(xiàn)象” 的原因是:SharedPreferences 事務中沒有保持 clear 變更記錄和 putValue 變更記錄的順序怜校,所以 clear 操作之前的 putValue 操作依然會生效。

示例程序

getSharedPreferences("user", Context.MODE_PRIVATE).let {
    it.edit().putString("name", "XIAOP PENG")
        .clear()
        .putString("age", "18")
        .apply()
}

小結一下 3 個映射表的區(qū)別:

  • 1注竿、mMap 是 SharedPreferencesImpl 對象中記錄的鍵值對數(shù)據(jù)茄茁,代表 SharedPreferences 的內(nèi)存緩存;
  • 2巩割、mModified 是 Editor 修改器中記錄的鍵值對變更記錄裙顽;
  • 3、mapToWriteToDisk 是 mMap 與 mModified 合并后宣谈,需要全量覆蓋寫回磁盤的數(shù)據(jù)愈犹。

6. 兩種寫回策略

在獲得事務對象后,我們繼續(xù)分析 Editor 接口中的 commit 同步寫回策略和 apply 異步寫回策略蒲祈。

6.1 commit 同步寫回策略

Editor#commit 同步寫回相對簡單甘萧,核心步驟分為 4 步:

  • 1、調(diào)用 commitToMemory() 創(chuàng)建 MemoryCommitResult 事務對象梆掸;
  • 2扬卷、調(diào)用 enqueueDiskWrite(mrc, null) 提交磁盤寫回任務(在當前線程執(zhí)行);
  • 3酸钦、調(diào)用 CountDownLatch#await() 阻塞等待磁盤寫回完成怪得;
  • 4、調(diào)用 notifyListeners() 觸發(fā)回調(diào)監(jiān)聽。

commit 同步寫回示意圖

其實嚴格來說徒恋,commit 同步寫回也不絕對是在當前線程同步寫回蚕断,也有可能在后臺 HandlerThread 線程寫回。但不管怎么樣入挣,對于 commit 同步寫回來說亿乳,都會調(diào)用 CountDownLatch#await() 阻塞等待磁盤寫回完成,所以在邏輯上也等價于在當前線程同步寫回径筏。

SharedPreferencesImpl.java

public final class EditorImpl implements Editor {

    @Override
    public boolean commit() {
        // 1葛假、獲取事務對象(前文已分析)
        MemoryCommitResult mcr = commitToMemory();
        // 2、提交磁盤寫回任務
        SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* 寫回成功回調(diào) */);
        // 3滋恬、阻塞等待寫回完成
        mcr.writtenToDiskLatch.await();
        // 4聊训、觸發(fā)回調(diào)監(jiān)聽器
        notifyListeners(mcr);
        return mcr.writeToDiskResult;
    }
}

6.2 apply 異步寫回策略

Editor#apply 異步寫回相對復雜,核心步驟分為 5 步:

  • 1恢氯、調(diào)用 commitToMemory() 創(chuàng)建 MemoryCommitResult 事務對象带斑;
  • 2、創(chuàng)建 awaitCommit Ruunnable 并提交到 QueuedWork 中勋拟。awaitCommit 中會調(diào)用 CountDownLatch#await() 阻塞等待磁盤寫回完成勋磕;
  • 3、創(chuàng)建 postWriteRunnable Runnable指黎,在 run() 中會執(zhí)行 awaitCommit 任務并將其從 QueuedWork 中移除朋凉;
  • 4、調(diào)用 enqueueDiskWrite(mcr, postWriteRunnable) 提交磁盤寫回任務(在子線程執(zhí)行)醋安;
  • 5、調(diào)用 notifyListeners() 觸發(fā)回調(diào)監(jiān)聽墓毒。

可以看到不管是調(diào)用 commit 還是 apply吓揪,最終都會調(diào)用 SharedPreferencesImpl#enqueueDiskWrite() 提交磁盤寫回任務。

區(qū)別在于:

  • 在 commit 中 enqueueDiskWrite() 的第 2 個參數(shù)是 null所计;
  • 在 apply 中 enqueueDiskWrite() 的第 2 個參數(shù)是一個 postWriteRunnable 寫回結束的回調(diào)對象柠辞,enqueueDiskWrite() 內(nèi)部就是根據(jù)第 2 個參數(shù)來區(qū)分 commit 和 apply 策略。

apply 異步寫回示意圖

SharedPreferencesImpl.java

@Override
public void apply() {
    // 1主胧、獲取事務對象(前文已分析)
    final MemoryCommitResult mcr = commitToMemory();
    // 2叭首、提交 aWait 任務
    // 疑問:postWriteRunnable 可以理解,awaitCommit 是什么踪栋?
    final Runnable awaitCommit = new Runnable() {
        @Override
        public void run() {
            // 阻塞線程直到磁盤任務執(zhí)行完畢
            mcr.writtenToDiskLatch.await();
        }
    };
    QueuedWork.addFinisher(awaitCommit);
    // 3焙格、創(chuàng)建寫回成功回調(diào)
    Runnable postWriteRunnable = new Runnable() {
        @Override
        public void run() {
            // 執(zhí)行 aWait 任務
            awaitCommit.run();
            // 移除 aWait 任務
            QueuedWork.removeFinisher(awaitCommit);
        }
    };

    // 4、提交磁盤寫回任務夷都,并綁定寫回成功回調(diào)
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable /* 寫回成功回調(diào) */);

    // 5眷唉、觸發(fā)回調(diào)監(jiān)聽器
    notifyListeners(mcr);
}

QueuedWork.java

// 提交 aWait 任務(后文詳細分析)
private static final LinkedList<Runnable> sFinishers = new LinkedList<>();

public static void addFinisher(Runnable finisher) {
    synchronized (sLock) {
        sFinishers.add(finisher);
    }
}

public static void removeFinisher(Runnable finisher) {
    synchronized (sLock) {
        sFinishers.remove(finisher);
    }
}

這里有一個疑問:

在 apply() 方法中,在執(zhí)行 enqueueDiskWrite() 前創(chuàng)建了 awaitCommit 任務并加入到 QueudWork 等待隊列,直到磁盤寫回結束才將 awaitCommit 移除冬阳。這個 awaitCommit 任務是做什么的呢蛤虐?

我們稍微再回答,先繼續(xù)往下走肝陪。

6.3 enqueueDiskWrite() 提交磁盤寫回事務

可以看到驳庭,不管是 commit 還是 apply,最終都會調(diào)用 SharedPreferencesImpl#enqueueDiskWrite() 提交寫回磁盤任務氯窍。雖然 enqueueDiskWrite() 還沒到真正調(diào)用磁盤寫回操作的地方嚷掠,但確實創(chuàng)建了與磁盤 IO 相關的 Runnable 任務,核心步驟分為 4 步:

  • 步驟 1:根據(jù)是否有 postWriteRunnable 回調(diào)區(qū)分是 commit 和 apply荞驴;
  • 步驟 2:創(chuàng)建磁盤寫回任務(真正執(zhí)行磁盤 IO 的地方):
    • 2.1 調(diào)用 writeToFile() 執(zhí)行寫回磁盤 IO 操作不皆;
    • 2.2 在寫回結束后對前文提到的 mDiskWritesInFlight 計數(shù)自減 1霹娄;
    • 2.3 執(zhí)行 postWriteRunnable 寫回成功回調(diào);
  • 步驟 3:如果是異步寫回犬耻,則提交到 QueuedWork 任務隊列枕磁;
  • 步驟 4:如果是同步寫回,則檢查 mDiskWritesInFlight 變量。如果存在并發(fā)寫回的事務,則也要提交到 QueuedWork 任務隊列,否則就直接在當前線程執(zhí)行。

其中步驟 2 是真正執(zhí)行磁盤 IO 的地方,邏輯也很好理解凝颇。不好理解的是袱饭,我們發(fā)現(xiàn)除了 “同步寫回而且不存在并發(fā)寫回事務” 這種特殊情況惑芭,其他情況都會交給 QueuedWork 再調(diào)度一次婴渡。

在通過 QueuedWork#queue 提交任務時假消,會將 writeToDiskRunnable 任務追加到 sWork 任務隊列中柠并。如果是首次提交任務,QueuedWork 內(nèi)部還會創(chuàng)建一個 HandlerThread 線程富拗,通過這個子線程實現(xiàn)異步的寫回任務臼予。這說明 SharedPreference 的異步寫回相當于使用了一個單線程的線程池,事實上在 Android 8.0 以前的版本中就是使用一個 singleThreadExecutor 線程池實現(xiàn)的啃沪。

提交任務示意圖

SharedPreferencesImpl.java

private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
    // 1粘拾、根據(jù)是否有 postWriteRunnable 回調(diào)區(qū)分是 commit 和 apply
    final boolean isFromSyncCommit = (postWriteRunnable == null);
    // 2、創(chuàng)建磁盤寫回任務
    final Runnable writeToDiskRunnable = new Runnable() {
        @Override
        public void run() {
            synchronized (mWritingToDiskLock) {
                // 2.1 寫入磁盤文件
                writeToFile(mcr, isFromSyncCommit);
            }
            synchronized (mLock) {
                // 2.2 mDiskWritesInFlight:進行中事務自減 1
                mDiskWritesInFlight--;
            }
            if (postWriteRunnable != null) {
                // 2.3 觸發(fā)寫回成功回調(diào)
                postWriteRunnable.run();
            }
        }
    };

    // 3创千、同步寫回且不存在并發(fā)寫回缰雇,則直接在當前線程
    // 這就是前文提到 “commit 也不是絕對在當前線程同步寫回” 的源碼出處
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (mLock) {
            // 如果存在并發(fā)寫回的事務,則此處 wasEmpty = false
            wasEmpty = mDiskWritesInFlight == 1;
        }
        // wasEmpty 為 true 說明當前只有一個線程在執(zhí)行提交操作签餐,那么就直接在此線程上完成任務
        if (wasEmpty) {
            writeToDiskRunnable.run();
            return;
        }
    }

    // 4寓涨、交給 QueuedWork 調(diào)度(同步任務不可以延遲)
    QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit /*是否可以延遲*/ );
}

@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    // 稍后分析
}

QueuedWork 調(diào)度:

QueuedWork.java

@GuardedBy("sLock")
private static LinkedList<Runnable> sWork = new LinkedList<>();

// 提交任務
// shouldDelay:是否延遲
public static void queue(Runnable work, boolean shouldDelay) {
    Handler handler = getHandler();

    synchronized (sLock) {
        // 入隊
        sWork.add(work);
        // 發(fā)送 Handler 消息,觸發(fā) HandlerThread 執(zhí)行任務
        if (shouldDelay && sCanDelay) {
            handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY /* 100ms */);
        } else {
            handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
        }
    }
}

private static Handler getHandler() {
    synchronized (sLock) {
        if (sHandler == null) {
            // 創(chuàng)建 HandlerThread 后臺線程
            HandlerThread handlerThread = new HandlerThread("queued-work-looper", Process.THREAD_PRIORITY_FOREGROUND);
            handlerThread.start();

            sHandler = new QueuedWorkHandler(handlerThread.getLooper());
        }
        return sHandler;
    }
}

private static class QueuedWorkHandler extends Handler {
    static final int MSG_RUN = 1;

    QueuedWorkHandler(Looper looper) {
        super(looper);
    }

    public void handleMessage(Message msg) {
        if (msg.what == MSG_RUN) {
            // 執(zhí)行任務
            processPendingWork();
        }
    }
}

private static void processPendingWork() {
    synchronized (sProcessingWork) {
        LinkedList<Runnable> work;

        synchronized (sLock) {
            // 創(chuàng)建新的任務隊列
            // 這一步是必須的氯檐,否則會與 enqueueDiskWrite 沖突
            work = sWork;
            sWork = new LinkedList<>();

            // Remove all msg-s as all work will be processed now
            getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
        }

        // 遍歷 戒良,按順序執(zhí)行 sWork 任務隊列
        if (work.size() > 0) {
            for (Runnable w : work) {
                w.run();
            }
        }
    }
}

比較不理解的是:

同一個文件的多次寫回串行化可以理解,對于多個文件的寫回串行化意義是什么冠摄,是不是可以用多線程來寫回多個不同的文件糯崎?或許這也是 SharedPreferences 是輕量級框架的原因之一,你覺得呢河泳?

6.4 主動等待寫回任務結束

現(xiàn)在我們可以回答 6.1 中遺留的問題:

在 apply() 方法中沃呢,在執(zhí)行 enqueueDiskWrite() 前創(chuàng)建了 awaitCommit 任務并加入到 QueudWork 等待隊列,直到磁盤寫回結束才將 awaitCommit 移除拆挥。這個 awaitCommit 任務是做什么的呢薄霜?

要理解這個問題需要管理分析到 ActivityThread 中的主線程消息循環(huán):

可以看到,在主線程的 Activity#onPause纸兔、Activity#onStop惰瓜、Service#onStop、Service#onStartCommand 等生命周期狀態(tài)變更時汉矿,會調(diào)用 QueudeWork.waitToFinish():

ActivityThread.java

@Override
public void handlePauseActivity(...) {
    performPauseActivity(r, finished, reason, pendingActions);
    // Make sure any pending writes are now committed.
    if (r.isPreHoneycomb()) {
        QueuedWork.waitToFinish();
    }
    ...
}

private void handleStopService(IBinder token) {
    ...
    QueuedWork.waitToFinish();
    ActivityManager.getService().serviceDoneExecuting(token, SERVICE_DONE_EXECUTING_STOP, 0, 0);
    ...
}

waitToFinish() 會執(zhí)行所有 sFinishers 等待隊列中的 aWaitCommit 任務崎坊,主動等待所有磁盤寫回任務結束。在寫回任務結束之前洲拇,主線程會阻塞在等待鎖上奈揍,這里也有可能發(fā)生 ANR曲尸。

主動等待示意圖

至于為什么 Google 要在 ActivityThread 中部分生命周期中主動等待所有磁盤寫回任務結束呢?官方并沒有明確表示男翰,結合頭條和抖音技術團隊的文章另患,我比較傾向于這 2 點解釋:

  • 解釋 1 - 跨進程同步(主要): 為了保證跨進程的數(shù)據(jù)同步,要求在組件跳轉前奏篙,確保當前組件的寫回任務必須在當前生命周期內(nèi)完成柴淘;
  • 解釋 2 - 數(shù)據(jù)完整性: 為了防止在組件跳轉的過程中可能產(chǎn)生的 Crash 造成未寫回的數(shù)據(jù)丟失,要求當前組件的寫回任務必須在當前生命周期內(nèi)完成秘通。

當然這兩個解釋并不全面为严,因為就算要求主動等待,也不能保證跨進程實時同步肺稀,也不能保證不產(chǎn)生 Crash第股。

抖音技術團隊觀點

QueuedWork.java

@GuardedBy("sLock")
private static Handler sHandler = null;

public static void waitToFinish() {
    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);
        }
        // We should not delay any work as this might delay the finishers
        sCanDelay = false;
    }

    // Android 8.0 優(yōu)化:幫助子線程執(zhí)行磁盤寫回
    // 作用有限,因為 QueuedWork 使用了 sProcessingWork 鎖保證同一時間最多只有一個線程在執(zhí)行磁盤寫回
    // 所以這里應該是嘗試在主線程執(zhí)行话原,可以提升線程優(yōu)先級
    processPendingWork();

    // 執(zhí)行 sFinshers 等待隊列夕吻,等待所有寫回任務結束
    try {
        while (true) {
            Runnable finisher;

            synchronized (sLock) {
                finisher = sFinishers.poll();
            }

            if (finisher == null) {
                break;
            }
            // 執(zhí)行 mcr.writtenToDiskLatch.await();
            // 阻塞線程直到磁盤任務執(zhí)行完畢
            finisher.run();
        }
    } finally {
        sCanDelay = true;
    }
}

Android 7.1 QueuedWork 源碼對比:

public static boolean hasPendingWork() {
    return !sPendingWorkFinishers.isEmpty();
}

7. writeToFile() 姍姍來遲

最終走到具體調(diào)用磁盤 IO 操作的地方了!

7.1 寫回步驟

writeToFile() 的邏輯相對復雜一些了繁仁。經(jīng)過簡化后涉馅,剩下的核心步驟只有 4 大步驟:

  • 步驟 1:過濾無效寫回事務:

    • 1.1 事務的 memoryStateGeneration 內(nèi)存版本小于 mDiskStateGeneration 磁盤版本,跳過黄虱;
    • 1.2 同步寫回必須寫回稚矿;
    • 1.3 異步寫回事務的 memoryStateGeneration 內(nèi)存版本版本小于 mCurrentMemoryStateGeneration 最新內(nèi)存版本,跳過捻浦。
  • 步驟 2:文件備份:

    • 2.1 如果不存在備份文件晤揣,則將舊文件重命名為備份文件;
    • 2.2 如果存在備份文件朱灿,則刪除無效的舊文件(上一次寫回出并且后處理沒有成功刪除的情況)昧识。
  • 步驟 3:全量覆蓋寫回磁盤:

    • 3.1 打開文件輸出流;
    • 3.2 將 mapToWriteToDisk 映射表全量寫出盗扒;
    • 3.3 調(diào)用 FileUtils.sync() 強制操作系統(tǒng)頁緩存寫回磁盤跪楞;
    • 3.4 寫入成功,則刪除被封文件(如果沒有走到這一步侣灶,在將來讀取文件時习霹,會重新恢復備份文件);
    • 3.5 將磁盤版本記錄為當前內(nèi)存版本炫隶;
    • 3.6 寫回結束(成功)。
  • 步驟 4:后處理: 刪除寫至半途的無效文件阎曹。

7.2 寫回優(yōu)化

繼續(xù)分析發(fā)現(xiàn)伪阶,SharedPreference 的寫回操作并不是簡單的調(diào)用磁盤 IO煞檩,在保證 “可用性” 方面也做了一些優(yōu)化設計:

  • 優(yōu)化 1 - 過濾無效的寫回事務:

如前文所述,commit 和 apply 都可能出現(xiàn)并發(fā)修改同一個文件的情況栅贴,此時在連續(xù)修改同一個文件的事務序列中斟湃,舊的事務是沒有意義的。為了過濾這些無意義的事務檐薯,在創(chuàng)建 MemoryCommitResult 事務對象時會記錄當時的 memoryStateGeneration 內(nèi)存版本凝赛,而在 writeToFile() 中就會根據(jù)這個字段過濾無效事務,避免了無效的 I/O 操作坛缕。

  • 優(yōu)化 2 - 備份舊文件:

由于寫回文件的過程存在不確定的異常(比如內(nèi)核崩潰或者機器斷電)墓猎,為了保證文件的完整性,SharedPreferences 采用了文件備份機制赚楚。在執(zhí)行寫回操作之前毙沾,會先將舊文件重命名為 .bak 備份文件,在全量覆蓋寫入新文件后再刪除備份文件宠页。

如果寫回文件失敗左胞,那么在后處理過程中會刪除寫至半途的無效文件。此時磁盤中只有一個備份文件举户,而真實文件需要等到下次觸發(fā)寫回事務時再寫回烤宙。

如果直到應用退出都沒有觸發(fā)下次寫回,或者寫回的過程中 Crash俭嘁,那么在前文提到的創(chuàng)建 SharedPreferencesImpl 對象的構造方法中調(diào)用 loadFromDisk() 讀取并解析文件數(shù)據(jù)時躺枕,會從備份文件恢復數(shù)據(jù)。

  • 優(yōu)化 3 - 強制頁緩存寫回:

在寫回文件成功后兄淫,SharedPreference 會調(diào)用 FileUtils.sync() 強制操作系統(tǒng)將頁緩存寫回磁盤屯远。

寫回示意圖

SharedPreferencesImpl.java

// 內(nèi)存版本
@GuardedBy("this")
private long mCurrentMemoryStateGeneration;

// 磁盤版本
@GuardedBy("mWritingToDiskLock")
private long mDiskStateGeneration;

// 寫回事務
private static class MemoryCommitResult {
    // 內(nèi)存版本
    final long memoryStateGeneration;
    // 需要全量覆蓋寫回磁盤的數(shù)據(jù)
    final Map<String, Object> mapToWriteToDisk;
    // 同步計數(shù)器
final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);

    // 后文寫回結束后調(diào)用
    // wasWritten:是否有執(zhí)行寫回
    // result:是否成功
    void setDiskWriteResult(boolean wasWritten, boolean result) {
        this.wasWritten = wasWritten;
        writeToDiskResult = result;
        // 喚醒等待鎖
        writtenToDiskLatch.countDown();
    }
}

// 提交寫回事務
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
    ...
    // 創(chuàng)建磁盤寫回任務
    final Runnable writeToDiskRunnable = new Runnable() {
        @Override
        public void run() {
            synchronized (mWritingToDiskLock) {
                // 2.1 寫入磁盤文件
                writeToFile(mcr, isFromSyncCommit);
            }
            synchronized (mLock) {
                // 2.2 mDiskWritesInFlight:進行中事務自減 1
                mDiskWritesInFlight--;
            }
            if (postWriteRunnable != null) {
                // 2.3 觸發(fā)寫回成功回調(diào)
                postWriteRunnable.run();
            }
        }
    };
    ...
}

// 寫回文件
// isFromSyncCommit:是否同步寫回
@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    boolean fileExists = mFile.exists();
    // 如果舊文件存在
    if (fileExists) { 
        // 1. 過濾無效寫回事務
        // 是否需要執(zhí)行寫回
        boolean needsWrite = false;

        // 1.1 磁盤版本小于內(nèi)存版本,才有可能需要寫回
        // (只有舊文件存在才會走到這個分支捕虽,但是舊文件不存在的時候也可能存在無意義的寫回慨丐,
        // 猜測官方是希望首次創(chuàng)建文件的寫回能夠及時盡快執(zhí)行,畢竟只有一個后臺線程)
        if (mDiskStateGeneration < mcr.memoryStateGeneration) {
            if (isFromSyncCommit) {
                // 1.2 同步寫回必須寫回
                needsWrite = true;
            } else {
                // 1.3 異步寫回需要判斷事務對象的內(nèi)存版本泄私,只有最新的內(nèi)存版本才有必要執(zhí)行寫回
                synchronized (mLock) {
                    if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
                        needsWrite = true;
                    }
                }
            }
        }

        if (!needsWrite) {
            // 1.4 無效的異步寫回房揭,直接結束
            mcr.setDiskWriteResult(false, true);
            return;
        }

        // 2. 文件備份
        boolean backupFileExists = mBackupFile.exists();
        if (!backupFileExists) {
            // 2.1 如果不存在備份文件,則將舊文件重命名為備份文件
            if (!mFile.renameTo(mBackupFile)) {
                // 備份失敗
                mcr.setDiskWriteResult(false, false);
                return;
            }
        } else {
            // 2.2 如果存在備份文件晌端,則刪除無效的舊文件(上一次寫回出并且后處理沒有成功刪除的情況)
            mFile.delete();
        }
    }

    try {
        // 3捅暴、全量覆蓋寫回磁盤
        // 3.1 打開文件輸出流
        FileOutputStream str = createFileOutputStream(mFile);
        if (str == null) {
            // 打開輸出流失敗
            mcr.setDiskWriteResult(false, false);
            return;
        }
        // 3.2 將 mapToWriteToDisk 映射表全量寫出
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
        // 3.3 FileUtils.sync:強制操作系統(tǒng)將頁緩存寫回磁盤
        FileUtils.sync(str);
        // 關閉輸出流
        str.close();
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);

        // 3.4 寫入成功,則刪除被封文件(如果沒有走到這一步咧纠,在將來讀取文件時蓬痒,會重新恢復備份文件)
        mBackupFile.delete();
        // 3.5 將磁盤版本記錄為當前內(nèi)存版本
        mDiskStateGeneration = mcr.memoryStateGeneration;
        // 3.6 寫回結束(成功)
        mcr.setDiskWriteResult(true, true);

        return;
    } catch (XmlPullParserException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    } catch (IOException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    }

    // 在 try 塊中拋出異常,會走到這里
    // 4漆羔、后處理:刪除寫至半途的無效文件
    if (mFile.exists()) {
        if (!mFile.delete()) {
            Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
        }
    }
    // 寫回結束(失斘嗌荨)
    mcr.setDiskWriteResult(false, false);
}

// -> 讀取并解析文件數(shù)據(jù)
private void loadFromDisk() {
    synchronized (mLock) {
        if (mLoaded) {
            return;
        }
        // 1狱掂、如果存在備份文件,則恢復備份數(shù)據(jù)(后文詳細分析)
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }
    ...
}

至此亲轨,SharedPreferences 核心源碼分析結束趋惨。


8. SharedPreferences 的其他細節(jié)

SharedPreferences 還有其他細節(jié)值得學習。

8.1 SharedPreferences 鎖總結

SharedPreferences 是線程安全的惦蚊,但它的線程安全并不是直接使用一個全局的鎖對象器虾,而是采用多種顆粒度的鎖對象實現(xiàn) “鎖細化” ,而且還貼心地使用了 @GuardedBy 注解標記字段或方法所述的鎖級別蹦锋。

使用 @GuardedBy 注解標記鎖級別

@GuardedBy("mLock")
private Map<String, Object> mMap;
對象鎖 功能呢 描述
1兆沙、SharedPreferenceImpl#mLock SharedPreferenceImpl 對象的全局鎖 全局使用
2、EditorImpl#mEditorLock EditorImpl 修改器的寫鎖 確保多線程訪問 Editor 的競爭安全
3晕粪、SharedPreferenceImpl#mWritingToDiskLock SharedPreferenceImpl#writeToFile() 的互斥鎖 writeToFile() 中會修改內(nèi)存狀態(tài)挤悉,需要保證多線程競爭安全
4、QueuedWork.sLock QueuedWork 的互斥鎖 確保 sFinishers 和 sWork 的多線程資源競爭安全
5巫湘、QueuedWork.sProcessingWork QueuedWork#processPendingWork() 的互斥鎖 確保同一時間最多只有一個線程執(zhí)行磁盤寫回任務

8.2 使用 WeakHashMap 存儲監(jiān)聽器

SharedPreference 提供了 OnSharedPreferenceChangeListener 回調(diào)監(jiān)聽器装悲,可以在主線程監(jiān)聽鍵值對的變更(包含修改、新增和移除)尚氛。

SharedPreferencesImpl.java

@GuardedBy("mLock")
private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners =
    new WeakHashMap<OnSharedPreferenceChangeListener, Object>();

SharedPreferences.java

public interface SharedPreferences {

    public interface OnSharedPreferenceChangeListener {
        void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
    }
}

比較意外的是: SharedPreference 使用了一個 WeakHashMap 弱鍵散列表存儲監(jiān)聽器诀诊,并且將監(jiān)聽器對象作為 Key 對象。這是為什么呢阅嘶?

這是一種防止內(nèi)存泄漏的考慮属瓣,因為 SharedPreferencesImpl 的生命周期是全局的(位于 ContextImpl 的內(nèi)存緩存),所以有必要使用弱引用防止內(nèi)存泄漏讯柔。想想也對抡蛙,Java 標準庫沒有提供類似 WeakArrayList 或 WeakLinkedList 的容器,所以這里將監(jiān)聽器對象作為 WeakHashMap 的 Key魂迄,就很巧妙的復用了 WeakHashMap 自動清理無效數(shù)據(jù)的能力粗截。

提示: 關于 WeakHashMap 的詳細分析,請閱讀小彭說 · 數(shù)據(jù)結構與算法 專欄文章 《WeakHashMap 和 HashMap 的區(qū)別是什么捣炬,何時使用熊昌?》

8.3 如何檢查文件被其他進程修改?

在讀取和寫入文件后記錄 mStatTimestamp 時間戳和 mStatSize 文件大小湿酸,在檢查時檢查這兩個字段是否發(fā)生變化

SharedPreferencesImpl.java

// 文件時間戳
@GuardedBy("mLock")
private StructTimespec mStatTimestamp;
// 文件大小
@GuardedBy("mLock")
private long mStatSize;

// 讀取文件
private void loadFromDisk() {
    ...
    mStatTimestamp = stat.st_mtim;
    mStatSize = stat.st_size;
    ...
}

// 寫入文件
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
    ...
    mStatTimestamp = stat.st_mtim;
    mStatSize = stat.st_size;
    ...
}

// 檢查文件
private boolean hasFileChangedUnexpectedly() {
    synchronized (mLock) {
        if (mDiskWritesInFlight > 0) {
            // If we know we caused it, it's not unexpected.
            if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected.");
            return false;
        }
    }

    // 讀取文件 Stat 信息
    final StructStat stat = Os.stat(mFile.getPath());

    synchronized (mLock) {
        // 檢查修改時間和文件大小
        return !stat.st_mtim.equals(mStatTimestamp) || mStatSize != stat.st_size;
    }
}

至此婿屹,SharedPreferences 全部源碼分析結束。


9. 總結

可以看到推溃,雖然 SharedPreferences 是一個輕量級的 K-V 存儲框架昂利,但的確是一個完整的存儲方案。從源碼分析中,我們可以看到 SharedPreferences 在讀寫性能页眯、可用性方面都有做一些優(yōu)化梯捕,例如:鎖細化、事務化窝撵、事務過濾、文件備份等襟铭,值得細細品味碌奉。

在下篇文章里,我們來盤點 SharedPreferences 中存在的 “缺點”寒砖,為什么 SharedPreferences 沒有乘上新時代的船只赐劣。請關注。


參考資料

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末魁兼,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子漠嵌,更是在濱河造成了極大的恐慌咐汞,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件儒鹿,死亡現(xiàn)場離奇詭異化撕,居然都是意外死亡,警方通過查閱死者的電腦和手機约炎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進店門植阴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人圾浅,你說我怎么就攤上這事掠手。” “怎么了狸捕?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵喷鸽,是天一觀的道長。 經(jīng)常有香客問我府寒,道長魁衙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任株搔,我火速辦了婚禮剖淀,結果婚禮上,老公的妹妹穿的比我還像新娘纤房。我一直安慰自己纵隔,他們只是感情好,可當我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著捌刮,像睡著了一般碰煌。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上绅作,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天芦圾,我揣著相機與錄音,去河邊找鬼俄认。 笑死个少,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的眯杏。 我是一名探鬼主播夜焦,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼岂贩!你這毒婦竟也來了茫经?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤萎津,失蹤者是張志新(化名)和其女友劉穎卸伞,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年夹囚,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片弃酌。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖儡炼,靈堂內(nèi)的尸體忽然破棺而出妓湘,到底是詐尸還是另有隱情,我是刑警寧澤乌询,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布榜贴,位于F島的核電站,受9級特大地震影響妹田,放射性物質發(fā)生泄漏唬党。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一鬼佣、第九天 我趴在偏房一處隱蔽的房頂上張望驶拱。 院中可真熱鬧,春花似錦晶衷、人聲如沸蓝纲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽税迷。三九已至永丝,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間箭养,已是汗流浹背慕嚷。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留露懒,地道東北人闯冷。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像懈词,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子辩诞,可洞房花燭夜當晚...
    茶點故事閱讀 43,527評論 2 349

推薦閱讀更多精彩內(nèi)容