Android 數(shù)據(jù)存儲(chǔ)知識(shí)梳理(3) - SharedPreference 源碼解析

一吕粹、概述

SharedPreferences在開(kāi)發(fā)當(dāng)中常被用作保存一些類(lèi)似于配置項(xiàng)這類(lèi)輕量級(jí)的數(shù)據(jù)型奥,它采用鍵值對(duì)的格式,將數(shù)據(jù)存儲(chǔ)在xml文件當(dāng)中,并保存在data/data/{應(yīng)用包名}/shared_prefs下:


今天我們就來(lái)一起研究一下SP的實(shí)現(xiàn)原理堕澄。

二、SP 源碼解析

2.1 獲取 SharedPreferences 對(duì)象

在通過(guò)SP進(jìn)行讀寫(xiě)操作時(shí)霉咨,首先需要獲得一個(gè)SharedPreferences對(duì)象蛙紫,SharedPreferences是一個(gè)接口,它定義了系列讀寫(xiě)的接口途戒,其實(shí)現(xiàn)類(lèi)為SharedPreferencesImpl坑傅、在實(shí)際過(guò)程中,我們一般通過(guò)Application喷斋、Activity唁毒、Service的下面這個(gè)方法來(lái)獲取SP對(duì)象:

public SharedPreferences getSharedPreferences(String name, int mode)

來(lái)獲取SharedPreferences實(shí)例,而它們最終都是調(diào)用到ContextImplgetSharedPreferences方法星爪,下面是整個(gè)調(diào)用的結(jié)構(gòu):


ContextImpl當(dāng)中浆西,SharedPreferences是以一個(gè)靜態(tài)雙重ArrayMap的結(jié)構(gòu)來(lái)保存的:

private static ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>> sSharedPrefs;

下面,我們看一下獲取SP實(shí)例的過(guò)程:

    public SharedPreferences getSharedPreferences(String name, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            if (sSharedPrefs == null) {
                sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
            }
            //1.第一個(gè)維度是包名.
            final String packageName = getPackageName();
            ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
            if (packagePrefs == null) {
                packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
                sSharedPrefs.put(packageName, packagePrefs);
            }
            //2.第二個(gè)維度就是調(diào)用get方法時(shí)傳入的name顽腾,并且如果已經(jīng)存在了那么直接返回
            sp = packagePrefs.get(name);
            if (sp == null) {
                File prefsFile = getSharedPrefsFile(name);
                sp = new SharedPreferencesImpl(prefsFile, mode);
                packagePrefs.put(name, sp);
                return sp;
            }
        }

        return sp;
    }

在上面近零,我們看到SharedPreferencesImpl的構(gòu)造傳入了一個(gè)和name相關(guān)聯(lián)的File,它就是我們?cè)诘谝还?jié)當(dāng)中所說(shuō)的xml文件,在構(gòu)造函數(shù)中久信,會(huì)去預(yù)先讀取這個(gè)xml文件當(dāng)中的內(nèi)容:

SharedPreferencesImpl(File file, int mode) {
        //..
        startLoadFromDisk(); //讀取xml文件的內(nèi)容
}

這里啟動(dòng)了一個(gè)異步的線(xiàn)程窖杀,需要注意的是這里會(huì)將標(biāo)志位mLoad置為false,后面我們會(huì)談到這個(gè)標(biāo)志的作用:

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

loadFromDiskLocked中裙士,將xml文件中的內(nèi)容保存到Map當(dāng)中入客,在讀取完畢之后,喚醒之前有可能阻塞的讀寫(xiě)線(xiàn)程:

    private Map<String, Object> mMap;

    private void loadFromDiskLocked() {
        //1.如果已經(jīng)在加載腿椎,那么返回.
        if (mLoaded) {
            return;
        }

        //...
        //2.最終保存到map當(dāng)中
        map = XmlUtils.readMapXml(str);
        mMap = map;

        //...
        //3.由于讀寫(xiě)操作只有在mLoaded變量為true時(shí)才可進(jìn)行痊项,因此它們有可能阻塞在調(diào)用讀寫(xiě)操作的方法上,因此這里需要喚醒它們酥诽。
        notifyAll();
    }

SP對(duì)象的獲取過(guò)程來(lái)看鞍泉,我們可以得出下面幾個(gè)結(jié)論:

  • 與某個(gè)name所對(duì)應(yīng)的SP對(duì)象需要等到調(diào)用getSharedPreferences才會(huì)被創(chuàng)建
  • 對(duì)于同一進(jìn)程而言,在Activity/Application/Service獲取SP對(duì)象時(shí)肮帐,如果name相同咖驮,它們實(shí)際上獲取到的是同一個(gè)SP對(duì)象
  • 由于使用的是靜態(tài)容器來(lái)保存,因此即使Activity/Service銷(xiāo)毀了训枢,它之前創(chuàng)建的SP對(duì)象也不會(huì)被釋放托修,而SP中的數(shù)據(jù)又是用Map來(lái)保存的,也就是說(shuō)恒界,我們只要調(diào)用了某個(gè)name相關(guān)聯(lián)的getSharedPreferences方法睦刃,那么和該name對(duì)應(yīng)的xml文件中的數(shù)據(jù)都會(huì)被讀到內(nèi)存當(dāng)中,并且一直到進(jìn)程被結(jié)束十酣。

2.2 通過(guò) SharedPreferences 進(jìn)行讀取操作

讀取的操作很簡(jiǎn)單涩拙,它其實(shí)就是從之間預(yù)先讀取的mMap當(dāng)中去取出對(duì)應(yīng)的數(shù)據(jù),以getBoolean為例:

    public boolean getBoolean(String key, boolean defValue) {
        synchronized (this) {
            awaitLoadedLocked();
            Boolean v = (Boolean)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

這里唯一需要關(guān)心的是awaitLoadedLocked方法:

    private void awaitLoadedLocked() {
        //這里如果判斷沒(méi)有加載完畢耸采,那么會(huì)進(jìn)入無(wú)限等待狀態(tài)
        while (!mLoaded) {
            try {
                wait();
            } catch (InterruptedException unused) {}
        }
    }

在這個(gè)方法中兴泥,會(huì)去檢查mLoaded標(biāo)志位是否為true,如果不為true虾宇,那么說(shuō)明沒(méi)有加載完畢搓彻,該線(xiàn)程會(huì)釋放它所持有的鎖,進(jìn)入等待狀態(tài)嘱朽,直到loadFromDiskLocked加載完xml文件中的內(nèi)容調(diào)用notifyAll()后旭贬,該線(xiàn)程才被喚醒。

從讀取操作來(lái)看搪泳,我們可以得出以下兩個(gè)結(jié)論:

  • 任何時(shí)刻讀取操作稀轨,讀取的都是內(nèi)存中的值,而并不是xml文件的值森书。
  • 在調(diào)用讀取方法時(shí)靶端,如果構(gòu)造函數(shù)中的預(yù)讀取線(xiàn)程沒(méi)有執(zhí)行完畢谎势,那么將會(huì)導(dǎo)致讀取的線(xiàn)程進(jìn)入等待狀態(tài)。

2.3 通過(guò) SharedPreferences 進(jìn)行寫(xiě)入操作

2.3.1 獲取 EditorImpl

當(dāng)我們需要通過(guò)SharedPreferences寫(xiě)入信息時(shí)杨名,那么首先需要通過(guò).edit()獲得一個(gè)Editor對(duì)象脏榆,這里和讀取操作類(lèi)似,都是需要等到預(yù)加載的線(xiàn)程執(zhí)行完畢:

    public Editor edit() {
        synchronized (this) {
            awaitLoadedLocked();
        }
        return new EditorImpl();
    }

Editor的實(shí)現(xiàn)類(lèi)為EditorImpl台谍,以putString為例:

    public final class EditorImpl implements Editor {

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

由上面的代碼可以看出须喂,當(dāng)我們調(diào)用EditorputXXX方法時(shí),實(shí)際上并沒(méi)有保存到SPmMap當(dāng)中趁蕊,而僅僅是保存到通過(guò).edit()返回的EditorImpl的臨時(shí)變量當(dāng)中坞生。

2.3.2 apply 和 commit 方法

我們通過(guò)editor寫(xiě)入的數(shù)據(jù),最終需要等到調(diào)用editorapplycommit方法掷伙,才會(huì)寫(xiě)入到內(nèi)存和xml這兩個(gè)地方掸茅。

(a) apply

下面汛兜,我們先看比較常用的apply方法:

        public void apply() {
            //1.將修改操作提交到內(nèi)存當(dāng)中.
            final MemoryCommitResult mcr = commitToMemory();
           
            //2.寫(xiě)入文件當(dāng)中
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); //postWriteRunnable在寫(xiě)入文件完成后進(jìn)行一些收尾操作.
            
            //3.只要寫(xiě)入到內(nèi)存當(dāng)中汁尺,就通知監(jiān)聽(tīng)者.
            notifyListeners(mcr);
        }

整個(gè)apply分為三個(gè)步驟:

  • 通過(guò)commitToMemory寫(xiě)入到內(nèi)存中
  • 通過(guò)enqueueDiskWrite寫(xiě)入到磁盤(pán)中
  • 通知監(jiān)聽(tīng)者

其中第一個(gè)步驟很好理解吨灭,就是根據(jù)editor中的內(nèi)容,確定哪些是需要更新的數(shù)據(jù)宙地,然后把SP當(dāng)中的mMap變量進(jìn)行更新摔认,之后將變化的內(nèi)容封裝成MemoryCommitResult結(jié)構(gòu)體。

我們主要看一下第二步宅粥,是如何寫(xiě)入磁盤(pán)當(dāng)中的:

    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        //1.寫(xiě)入磁盤(pán)任務(wù)的runnable.
        final Runnable writeToDiskRunnable = new Runnable() {
                public void run() {
                    //1.1 寫(xiě)入磁盤(pán)
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr);
                    }
                    //....執(zhí)行收尾操作.
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };
        
        //2.這里如果是通過(guò)apply方法調(diào)用過(guò)來(lái)的参袱,那么為false
        final boolean isFromSyncCommit = (postWriteRunnable == null);

        if (isFromSyncCommit) { //apply 方法不走這里
                //...
                writeToDiskRunnable.run();
                return;
        }

        QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
    }

可以看出,如果調(diào)用apply方法秽梅,那么對(duì)于xml文件的寫(xiě)入是在異步線(xiàn)程當(dāng)中進(jìn)行的抹蚀。

(b) commit

如果調(diào)用的commit方法,那么執(zhí)行的是如下操作:

       public boolean commit() {
            //1.寫(xiě)入內(nèi)存
            MemoryCommitResult mcr = commitToMemory();
            //2.寫(xiě)入文件
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null); //由于是同步進(jìn)行风纠,所以把收尾操作放到Runnable當(dāng)中.
            //在這里執(zhí)行收尾操作..
            //3.通知監(jiān)聽(tīng)
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
        }

當(dāng)使用commit方法時(shí)况鸣,和apply類(lèi)似牢贸,都是三步操作竹观,只不過(guò)第二步在寫(xiě)入文件的時(shí)候,傳入的Runnablenull潜索,因此臭增,對(duì)于寫(xiě)入文件的操作是同步的,因此竹习,如果我們?cè)谥骶€(xiàn)程當(dāng)中調(diào)用了commit方法誊抛,那么實(shí)際上是在主線(xiàn)程進(jìn)行IO操作。

(c) 回調(diào)時(shí)機(jī)

  • 對(duì)于apply方法整陌,由于它對(duì)于文件的寫(xiě)入是異步的拗窃,但是notifyListener方法不會(huì)等到真正寫(xiě)入完成時(shí)才通知監(jiān)聽(tīng)者瞎领,因此監(jiān)聽(tīng)者在收到回調(diào)或者apply返回時(shí),對(duì)于SP數(shù)據(jù)的改變只是寫(xiě)入到了內(nèi)存當(dāng)中随夸,并沒(méi)有寫(xiě)入到文件當(dāng)中九默。
  • 對(duì)于commit方法,由于它對(duì)于文件的寫(xiě)入是同步的宾毒,因此可以保證監(jiān)聽(tīng)者收到回調(diào)時(shí)或者commit方法返回后驼修,改變已經(jīng)被寫(xiě)入到了文件當(dāng)中。

2.4 監(jiān)聽(tīng) SP 的變化

如果希望監(jiān)聽(tīng)SP的變化诈铛,那么可以通過(guò)下面的這兩個(gè)方法:

    public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
        synchronized(this) {
            mListeners.put(listener, mContent);
        }
    }

    public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
        synchronized(this) {
            mListeners.remove(listener);
        }
    }

由于對(duì)應(yīng)于NameSP在進(jìn)程中是實(shí)際上是一個(gè)單例模式乙各,因此,我們可以做到在進(jìn)程中的任何地方改變SP的數(shù)據(jù)幢竹,都能收到監(jiān)聽(tīng)耳峦。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市焕毫,隨后出現(xiàn)的幾起案子妇萄,更是在濱河造成了極大的恐慌,老刑警劉巖咬荷,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件冠句,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡幸乒,警方通過(guò)查閱死者的電腦和手機(jī)懦底,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)罕扎,“玉大人聚唐,你說(shuō)我怎么就攤上這事∏徽伲” “怎么了杆查?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)臀蛛。 經(jīng)常有香客問(wèn)我亲桦,道長(zhǎng),這世上最難降的妖魔是什么浊仆? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任客峭,我火速辦了婚禮,結(jié)果婚禮上抡柿,老公的妹妹穿的比我還像新娘舔琅。我一直安慰自己,他們只是感情好洲劣,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布备蚓。 她就那樣靜靜地躺著课蔬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪郊尝。 梳的紋絲不亂的頭發(fā)上购笆,一...
    開(kāi)封第一講書(shū)人閱讀 49,749評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音虚循,去河邊找鬼同欠。 笑死,一個(gè)胖子當(dāng)著我的面吹牛横缔,可吹牛的內(nèi)容都是我干的铺遂。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼茎刚,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼襟锐!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起膛锭,我...
    開(kāi)封第一講書(shū)人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤粮坞,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后初狰,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體莫杈,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年奢入,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了筝闹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡腥光,死狀恐怖关顷,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情武福,我是刑警寧澤议双,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布,位于F島的核電站捉片,受9級(jí)特大地震影響平痰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜界睁,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一觉增、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧翻斟,春花似錦、人聲如沸说铃。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至债热,卻和暖如春砾嫉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背窒篱。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工焕刮, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人墙杯。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓配并,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親高镐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子溉旋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

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

  • Android上常見(jiàn)的數(shù)據(jù)存儲(chǔ)方式有哪些呢? SharedPreferences這種存儲(chǔ)數(shù)據(jù)的方式我們平時(shí)用的都對(duì)...
    編程小豬閱讀 4,584評(píng)論 0 5
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,734評(píng)論 25 707
  • 面試題總結(jié) 通用 安卓學(xué)習(xí)途徑, 尋找資料學(xué)習(xí)的博客網(wǎng)站 AndroidStudio使用, 插件使用 安卓和蘋(píng)果的...
    JingBeibei閱讀 1,653評(píng)論 2 21