終于看懂了 SharedPreferences 的源碼實(shí)現(xiàn)

本文目錄:

  • 寫在前面
  • 獲取 SharedPreferences 實(shí)例
  • 加載 xml 數(shù)據(jù)文件
  • 初次讀取數(shù)據(jù)的耗時(shí)分析
  • commit 和 apply 的對(duì)比

寫在前面

SharedPreferences 平時(shí)用來(lái)持久化一些基本數(shù)據(jù)類型或者一些可序列化的對(duì)象。

根據(jù)我們?nèi)粘5慕?jīng)常,持久化操作是耗時(shí)的烙样,涉及到文件的 IO 操作,但是實(shí)際使用 SharedPreferences 時(shí)娶聘,發(fā)現(xiàn)只有第一次讀取數(shù)據(jù)是有概率卡主線程幾十到幾百毫秒,而之后的讀取時(shí)間幾乎可以忽略不計(jì)。

我們有了這樣的疑問:

  • 為什么初次讀取數(shù)據(jù)會(huì)有概率的阻塞?
  • 為什么除了初次讀取數(shù)據(jù)可能阻塞椎工,而可以在后面的讀取很快?
  • 為什么都推薦使用 apply 而不是 commit 提交數(shù)據(jù)蜀踏?

帶著問題去理解它的實(shí)現(xiàn)维蒙。

獲取 SharedPreferences 實(shí)例

SharedPreferences 是由 Context 返回的,比如我們的 Application果覆,Activity颅痊。所以具體的實(shí)現(xiàn)每個(gè)應(yīng)用的上下文環(huán)境有關(guān),每個(gè)應(yīng)用有自己的單獨(dú)的文件夾存放這些數(shù)據(jù)局待,對(duì)其他應(yīng)用不可見斑响。

獲取 SharedPreferences 的方法定義在抽象類 Context 中:

    public abstract SharedPreferences getSharedPreferences(String name, int mode);
    public abstract SharedPreferences getSharedPreferences(File file, int mode);

如果查看 Application 或者 Activity 的源碼,會(huì)找不到具體的實(shí)現(xiàn)钳榨。這是因?yàn)樗鼈兝^承了 ContextWrapper舰罚,代理模式,代理 ContextImpl 的實(shí)例 mBase 中重绷。ContextImpl 是具體的實(shí)現(xiàn)沸停。

兩種獲取 SharedPreferences 的方法中,我們基本上用的是 getSharedPreferences(String name, int mode); 昭卓,參數(shù)只傳了文件的名字愤钾。

查看內(nèi)部的代碼可以看到,雖然只有一個(gè)名字候醒,ContextImpl 會(huì)構(gòu)建出文件的具體路徑能颁。再接著調(diào)用 getSharedPreferences(File file, int mode); 方法返回 SharedPreferencesImpl 實(shí)例。

所以 SharedPreferences 的操作倒淫,本質(zhì)上就是對(duì)文件的操作伙菊。最后會(huì)落實(shí)到一個(gè) xml 文件上:

    @Override
    public File getSharedPreferencesPath(String name) {
        return makeFilename(getPreferencesDir(), name + ".xml");
    }

標(biāo)準(zhǔn)路徑在 /data/data/應(yīng)用包名/shared_prefs 文件夾中,且都是 xml 文件。

SharedPreferences 文件.png

創(chuàng)建好 File 對(duì)象后镜硕,會(huì)在 getSharedPreferences(File file, int mode) 中打開文件并執(zhí)行初始操作运翼,把 SharedPreferencesImpl 實(shí)例返回:

    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        checkMode(mode);
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            if (sp == null) {
                sp = new SharedPreferencesImpl(file, mode);
                cache.put(file, sp);
                return sp;
            }
        }
        if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
            getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
            sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
    }

這里我們看到了另一個(gè)重要的類 SharedPreferencesImpl,它和 ContextImpl 一樣兴枯,是接口的具體實(shí)現(xiàn)類血淌。

每一個(gè) File 文件對(duì)應(yīng)一個(gè) SharedPreferencesImpl 實(shí)例。為了提高效率财剖,ContextImpl 有做緩存 cache悠夯,這里的緩存是強(qiáng)引用,在整個(gè)進(jìn)程的生命周期中都存在躺坟,意味著每個(gè)文件的 SharedPreferencesImpl 實(shí)例在整個(gè)進(jìn)程中只會(huì)被創(chuàng)建一次沦补。

這個(gè)方法的末尾有一個(gè)特殊的處理需要注意一下,是關(guān)于模式 Context.MODE_MULTI_PROCESS 咪橙,可以看到在這個(gè)模式下夕膀,會(huì)調(diào)用:

    sp.startReloadIfChangedUnexpectedly();

這個(gè)方法執(zhí)行下去,會(huì)檢查文件是否被修改了匣摘,如果文件被修改了店诗,會(huì)調(diào)用 startLoadFromDisk 來(lái)更新文件。因?yàn)槎噙M(jìn)程環(huán)境下音榜,這里的文件有可能被其他進(jìn)程修改庞瘸。

加載 xml 數(shù)據(jù)文件

為什么除了初次讀取數(shù)據(jù)可能卡頓,而可以在后面的讀取很快赠叼?

我們進(jìn)入 SharedPreferences 的加載流程擦囊,就是把文件的內(nèi)容載入內(nèi)存的過程。

載入文件的方法在 startLoadFromDisk 中嘴办,顧名思義贯被,就是開始從磁盤加載數(shù)據(jù)彤灶。

調(diào)用該方法有兩個(gè)地方:

  • 構(gòu)造函數(shù)里會(huì)被調(diào)用汽煮。所以第一次創(chuàng)建 SharedPreferencesImpl 會(huì)馬上把文件內(nèi)容載入內(nèi)存心例。
  • Context.MODE_MULTI_PROCESS 下摆寄,文件發(fā)生修改時(shí)被調(diào)用逗扒。目的就是多進(jìn)程下更新數(shù)據(jù)现恼。

startLoadFromDisk 方法如下:

    private void startLoadFromDisk() {
        synchronized (this) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }

可以看到直接開啟一個(gè)新線程,調(diào)用 loadFromDisk 加載文件:

    private void loadFromDisk() {
        ...
        str = new BufferedInputStream(
                new FileInputStream(mFile), 16*1024);
        map = XmlUtils.readMapXml(str);
        ...
    }

本質(zhì)上棵里,就是讀取一個(gè) xml 文件,被內(nèi)容解析為 Map 對(duì)象。這個(gè) map 包含了我們之前保存的所有鍵值對(duì)的數(shù)據(jù)。并且把 map對(duì)象保存為 mMap 成員變量,直接在內(nèi)存中常駐:

    synchronized (SharedPreferencesImpl.this) {
        mLoaded = true;
        if (map != null) {
            mMap = map
            ...
        } else {
            mMap = new HashMap<>();
        }
        notifyAll();
    }

這里可以解釋我們的疑問,為什么 SharedPreferences 的讀取非常快,載入完成后,后面的讀操作都是針對(duì) mMap 的郁岩,響應(yīng)速度是內(nèi)存級(jí)別的非称继快片酝。比如 getString:

    public String getString(String key, @Nullable String defValue) {
        synchronized (this) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

我們也就可以理解為什么 SharedPreferences 不希望存大量數(shù)據(jù)了审轮,一個(gè)很重要的原因也是內(nèi)存緩存崖飘,如果數(shù)據(jù)量很大的話达椰,這里會(huì)占據(jù)很大一塊內(nèi)存檀何。

初次讀取數(shù)據(jù)的耗時(shí)分析

為什么初次讀取數(shù)據(jù)會(huì)有概率的阻塞?

對(duì)應(yīng)用性能監(jiān)控中發(fā)現(xiàn),SharedPreferences 初次讀取數(shù)據(jù)的會(huì)發(fā)現(xiàn)概率發(fā)生阻塞羡藐,一般會(huì)被卡 20~40 ms瘩扼。如果系統(tǒng) IO 原本就繁忙的話谆棺,甚至可能會(huì)卡好幾秒蔼啦。

所以在應(yīng)用啟動(dòng)中奈籽,我們?nèi)カ@取一些配置,不得不在主線程對(duì) SharedPreferences 進(jìn)行初次操作猛计。如果在短時(shí)間內(nèi)讀取多個(gè) 不同的 SharedPreferences唠摹,應(yīng)用的啟動(dòng)會(huì)耗費(fèi)很長(zhǎng)的時(shí)間。

這和一個(gè)鎖有關(guān)奉瘤,就是 SharePreferencesImpl.this勾拉。在初始化加載文件的時(shí)候,和讀取數(shù)據(jù)的時(shí)候都會(huì)用到這個(gè)鎖盗温。

在 SharePreferencesImpl 構(gòu)造中藕赞,調(diào)用 loadFromDisk ,加鎖保護(hù)了對(duì) mLoad 和 mMap 的讀寫:

    private void loadFromDisk() {
        synchronized (SharedPreferencesImpl.this) {
            ...
        }
    }

而每次讀取數(shù)據(jù)的時(shí)候卖局,也加了這個(gè)鎖去保護(hù)這些成員斧蜕,比如 getString:

    public String getString(String key, @Nullable String defValue) {
        synchronized (this) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

所以這里形成了一個(gè)競(jìng)爭(zhēng)關(guān)系,如果在本地 xml 文件的加載過程中砚偶,先執(zhí)行了 loadFromDisk批销,那么 getString 就會(huì)阻塞等待。

loadFromDisk 是 IO 耗時(shí)操作染坯,雖然 loadFromDisk 操作被分配到另一個(gè)線程執(zhí)行均芽,但因?yàn)樽x取數(shù)據(jù)的時(shí)候,爭(zhēng)用了這個(gè)鎖单鹿,會(huì)發(fā)生概率卡頓掀宋。

commit 和 apply 的對(duì)比

為什么都推薦使用 apply 而不是 commit 提交數(shù)據(jù)?

先看我們平時(shí)修改 SharedPreferences 的姿勢(shì):

    SharedPreferences sp = context.getSharedPreferences("test", Mode.PRIVATE);
    Editor editor = sp.edit();
    editor.putString("key", "Hello World!");
    editor.commit(); 或者 editor.apply();

可以看到具體修改被它的內(nèi)部類 EditorImpl 接管仲锄,最后才調(diào)用 commit 或者 apply劲妙,而這兩者的區(qū)別就是我們要討論的。

EditorImpl 內(nèi)部有一個(gè)內(nèi)存緩存儒喊,用來(lái)保存用戶修改后的操作:

    private final Map<String, Object> mModified = Maps.newHashMap();

在執(zhí)行 commit 或者 apply 前镣奋,比如上面的 editor.putString("key","Hello World!") 會(huì)把修改存儲(chǔ)在 mModified 中:

    public Editor putString(String key, @Nullable String value) {
        synchronized (this) {
            mModified.put(key, value);
                return this;
            }
        }
    }

到這里,只是把修改緩存在了內(nèi)存中澄惊。然后調(diào)用 commit 和 apply 把修改持久化唆途。

這兩個(gè)方法都會(huì)調(diào)用一個(gè) commitToMemory 方法,做兩件事情:

  • 一個(gè)是把修改提交到內(nèi)存
  • 創(chuàng)建 MemoryCommitResult 用來(lái)做后面的本地 IO掸驱。

修改很簡(jiǎn)單肛搬,就是遍歷 mModified,把修改的內(nèi)容全部同步給 mMap毕贼。

    for (Map.Entry<String, Object> e : mModified.entrySet()) {
        String k = e.getKey();
        Object v = e.getValue();
        // "this" is the magic value for a removal mutation. In addition,
        // setting a value to "null" for a given key is specified to be
        // equivalent to calling remove on that key.
        if (v == this || v == null) {
            if (!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);
        }
    }

而 MemoryCommitResult 是一個(gè)數(shù)據(jù)容器温赔,記錄著一些后面進(jìn)行磁盤寫入操作需要使用到的數(shù)據(jù),比如有:

  • boolean changesMade 鬼癣,標(biāo)記變量陶贼,用來(lái)標(biāo)記數(shù)據(jù)是否發(fā)生改變啤贩。
  • Map<?, ?> mapToWriteToDisk , 最終要寫入到本地的數(shù)據(jù)拜秧,會(huì)指向 SharedPreferencesImpl 的內(nèi)存緩存 mMap

同步修改到 mMap

經(jīng)過這個(gè)階段痹屹,內(nèi)存的數(shù)據(jù)就被更新了。并創(chuàng)建好 MemoryCommitResult 對(duì)象后枉氮,接下來(lái)就是不一樣的操作志衍。

先看 commit 方法:

    public boolean commit() {
        MemoryCommitResult mcr = commitToMemory();
        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;
    }

在調(diào)用 enqueueDiskWrite 的時(shí)候,因?yàn)闆]有構(gòu)建 postWriteRunnable聊替,最終會(huì)在當(dāng)前線程直接執(zhí)行寫入操作:

    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        ...
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (SharedPreferencesImpl.this) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }
        ...
    }

直接調(diào)用 writeToDiskRunnable.run() 沒有再開線程楼肪,直接阻塞寫入。

apply 方法:

        public void apply() {
            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);
                    }
                };

            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);
        }

可以看到先構(gòu)造了一個(gè) postWriteRunnable 傳入 enqueueDiskWrite惹悄。

在方法的執(zhí)行中春叫,可以看到最后會(huì)在一個(gè)單線程線程池 QueuedWork.singleThreadExecutor() 中執(zhí)行寫入操作:

    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        ...
        QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
    }

所以,commit 是阻塞的泣港,apply 是非阻塞的暂殖。

平時(shí)使用的時(shí)候,盡量使用 apply 避免卡主主線程当纱。因?yàn)閷懭肭岸家呀?jīng)更新修改到緩存了央星,不用擔(dān)心讀到臟數(shù)據(jù)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末惫东,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子毙石,更是在濱河造成了極大的恐慌廉沮,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件徐矩,死亡現(xiàn)場(chǎng)離奇詭異滞时,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)滤灯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門坪稽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人鳞骤,你說我怎么就攤上這事窒百。” “怎么了豫尽?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵篙梢,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我美旧,道長(zhǎng)渤滞,這世上最難降的妖魔是什么贬墩? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮妄呕,結(jié)果婚禮上陶舞,老公的妹妹穿的比我還像新娘。我一直安慰自己绪励,他們只是感情好肿孵,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著优炬,像睡著了一般颁井。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蠢护,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天雅宾,我揣著相機(jī)與錄音,去河邊找鬼葵硕。 笑死眉抬,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的懈凹。 我是一名探鬼主播蜀变,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼介评!你這毒婦竟也來(lái)了库北?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤们陆,失蹤者是張志新(化名)和其女友劉穎寒瓦,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體坪仇,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡杂腰,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了椅文。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片喂很。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖皆刺,靈堂內(nèi)的尸體忽然破棺而出少辣,到底是詐尸還是另有隱情,我是刑警寧澤芹橡,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布毒坛,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏煎殷。R本人自食惡果不足惜屯伞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望豪直。 院中可真熱鬧劣摇,春花似錦、人聲如沸弓乙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)礁阁。三九已至廊宪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間懈玻,已是汗流浹背巧婶。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留涂乌,地道東北人艺栈。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像湾盒,于是被迫代替她去往敵國(guó)和親湿右。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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