android SharedPreferences源碼解析

總體認(rèn)識(shí)

image

圖片來(lái)自掘金文章,我就懶得再畫(huà)一次了哈婚脱。

SharedPreferences是個(gè)接口叨叙,SharedPreferenceImpl是其實(shí)現(xiàn)。

而Editor是SharedPreferences的內(nèi)部類填硕,也是一個(gè)接口。EditorImpl是其實(shí)現(xiàn)鹿鳖,它是SharedPreferenceImpl的內(nèi)部類扁眯。

看看它們都提供了什么方法:

image.png

可以看到它支持5種基本數(shù)據(jù)類型的存取:boolean , int, float, long, String,和一個(gè)Set集合:Set<String>翅帜。

基本用法

    private void simpleSP(Context context) {
        
        SharedPreferences sp = context.getSharedPreferences("leonxtp", MODE_PRIVATE);
        SharedPreferences.Editor editor = sp.edit();
        editor.putString("my_key", "leonxtp");
//        editor.commit();
        editor.apply();

        String leonxtp = sp.getString("my_key", "");
        Log.i("SP", leonxtp);
    }

SharedPreferences是怎么存的姻檀?

磁盤(pán)上:

數(shù)據(jù)以xml形式存儲(chǔ)在/data/data/項(xiàng)目包名/shared_prefs/sp_name.xml

image

圖片來(lái)自簡(jiǎn)書(shū)文章

內(nèi)存中:

SharedPreferenceImpl.java:

final class SharedPreferencesImpl implements SharedPreferences {

    @GuardedBy("ContextImpl.class")
    private ArrayMap<String, File> mSharedPrefsPaths;

    @GuardedBy("ContextImpl.class")
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

    @GuardedBy("mLock")
    private Map<String, Object> mMap;

    public final class EditorImpl implements Editor {

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

    }
}

他們的存儲(chǔ)關(guān)系是:

  • 從文件的角度來(lái)說(shuō),

    一個(gè)包名對(duì)應(yīng)一個(gè)存儲(chǔ)目錄涝滴,里面有多個(gè)xml文件施敢,一個(gè)xml文件對(duì)應(yīng)一個(gè)map周荐,里面多個(gè)key-value

  • 從內(nèi)存的角度來(lái)說(shuō),

    sSharedPrefsCache中保存的是app中所有的xml文件對(duì)應(yīng)的操作它的SharedPreferencesImpl的對(duì)象僵娃,據(jù)查源碼概作,它內(nèi)部只保存了一個(gè)鍵值對(duì),key就是app的包名默怨。

    mSharedPrefsPaths保存的是文件名-文件列表讯榕,

    mMap保存的是某個(gè)xml文件對(duì)應(yīng)的內(nèi)容。

    mModified中保存的是修改過(guò)的內(nèi)容匙睹,它會(huì)在commit和apply之后愚屁,保存到mMap中,并且同步/異步寫(xiě)進(jìn)xml文件里痕檬。

    Android這樣設(shè)計(jì)的目的就是避免每次讀寫(xiě)都去操作文件霎槐,帶來(lái)很大的性能消耗。

OK梦谜,總體上的認(rèn)知就到這吧丘跌,開(kāi)始上源碼

源碼分析


我們通過(guò)context來(lái)獲取,而ContextImpl是它的實(shí)現(xiàn)唁桩,這個(gè)過(guò)程是怎么完成的闭树,可以看我之前的文章。

我們直接來(lái)到ContextImpl.java:

    @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.
        // 這里谷歌開(kāi)發(fā)人員寫(xiě)了一段感覺(jué)很自?shī)首詷?lè)的注釋荒澡,我們可以看到报辱,它允許null值哦
        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) {
                file = getSharedPreferencesPath(name);
                mSharedPrefsPaths.put(name, file);
            }
        }
        return getSharedPreferences(file, mode);
    }

先看內(nèi)存中有沒(méi)有這個(gè)xml文件的對(duì)象,沒(méi)有就創(chuàng)建单山,加入到 mSharedPrefsPaths中碍现。創(chuàng)建的代碼如下:

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

    private File makeFilename(File base, String name) {
        if (name.indexOf(File.separatorChar) < 0) {
            return new File(base, name);
        }
        throw new IllegalArgumentException(
                "File " + name + " contains a path separator");
    }

很簡(jiǎn)單,其中g(shù)etPreferencesDir()獲取到目錄就是/data/data/我們的程序包名/shared_prefs/米奸。

有了文件之后昼接,就嘗試去取出SharedPreferenceImpl對(duì)象了:

    @Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            if (sp == null) {
                checkMode(mode);
                // ...

                // 創(chuàng)建的時(shí)候,會(huì)將xml文件加載到mMap中
                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) {
            // If somebody else (some other process) changed the prefs
            // file behind our back, we reload it.  This has been the
            // historical (if undocumented) behavior.
            // 這里主要針對(duì)mode為多進(jìn)程的時(shí)候
            sp.startReloadIfChangedUnexpectedly();
        }
        return sp;
    }

創(chuàng)建SharedPreferenceImpl的時(shí)候會(huì)將xml文件加載到mMap中躏升,過(guò)程如下:

final class SharedPreferencesImpl implements SharedPreferences {

    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        startLoadFromDisk();
    }

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

    private void loadFromDisk() {
        synchronized (mLock) {
            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 {
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16*1024);
                    map = XmlUtils.readMapXml(str);
                } catch (Exception e) {
                    Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
            /* ignore */
        }

        synchronized (mLock) {
            mLoaded = true;
            if (map != null) {
                mMap = map;
                mStatTimestamp = stat.st_mtim;
                mStatSize = stat.st_size;
            } else {
                mMap = new HashMap<>();
            }
            mLock.notifyAll();
        }
    }

}

這里看到辩棒,它使用了同步代碼塊保證多線程的同步狼忱。
而mMap就是從xml文件中解析出來(lái)的膨疏。進(jìn)去看看,還是不看了吧钻弄,就是用了XmlParser解析而已佃却。

OK,獲染桨场(創(chuàng)建)的過(guò)程看完了饲帅,下面看看put操作复凳,

    public final class EditorImpl implements Editor {

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

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

就這么簡(jiǎn)單?灶泵?是的育八,畢竟,真正的操作在commit/apply里:

      public boolean commit() {
            long startTime = 0;

            if (DEBUG) {
                startTime = System.currentTimeMillis();
            }

            MemoryCommitResult mcr = commitToMemory();

            SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null /* sync write on this thread okay */);
            try {
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException e) {
                return false;
            } finally {
                if (DEBUG) {
                    Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                            + " committed after " + (System.currentTimeMillis() - startTime)
                            + " ms");
                }
            }
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
        }

注意這兩行:

            SharedPreferencesImpl.this.enqueueDiskWrite(
                mcr, null /* sync write on this thread okay */);
            try {
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException e) {
                return false;
            }

writtenToDiskLatch是一個(gè)CountDownLatch對(duì)象赦邻,它將會(huì)等待enqueueDiskWrite()方法執(zhí)行完成髓棋,這就會(huì)致使線程阻塞。所以commit操作寫(xiě)入文件是同步的惶洲,其中commitToMemory是將mModified中的修改寫(xiě)入到內(nèi)存中按声。

        // Returns true if any changes were made
        private MemoryCommitResult commitToMemory() {

            synchronized (SharedPreferencesImpl.this.mLock) {
                
                mapToWriteToDisk = mMap;

                boolean hasListeners = mListeners.size() > 0;

                synchronized (mLock) {

                    for (Map.Entry<String, Object> e : mModified.entrySet()) {
                        String k = e.getKey();
                        Object v = e.getValue();

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

                    mModified.clear();

                }
            }
            return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);
        }

而寫(xiě)入文件操作在enqueueDiskWrite()里:

    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        final boolean isFromSyncCommit = (postWriteRunnable == null);

        final Runnable writeToDiskRunnable = new Runnable() {
                public void run() {
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr, isFromSyncCommit);
                    }
                    synchronized (mLock) {
                        mDiskWritesInFlight--;
                    }
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };

        // Typical #commit() path with fewer allocations, doing a write on
        // the current thread.
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (mLock) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();
                return;
            }
        }

        QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
    }

可以看到,它開(kāi)了一個(gè)線程專門(mén)去寫(xiě)入文件恬吕,所以有些人說(shuō)commit操作是在主線程執(zhí)行的签则,其實(shí)并不是,它只是讓主線程等待子線程完成寫(xiě)入而已铐料。

那么apply()方法:

        public void apply() {
            final long startTime = System.currentTimeMillis();

            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                    public void run() {
                        try {
                            mcr.writtenToDiskLatch.await();
                        } catch (InterruptedException ignored) {
                        }

                    }
                };

            QueuedWork.addFinisher(awaitCommit);

            Runnable postWriteRunnable = new Runnable() {
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };

            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

            notifyListeners(mcr);
        }

它跟commit不同的地方就是渐裂,不在當(dāng)前線程等待文件寫(xiě)入線程執(zhí)行完畢,而是開(kāi)了一個(gè)線程余赢,并且沒(méi)有返回值芯义。所以它對(duì)于不需要返回值的調(diào)用者來(lái)說(shuō),效率更高妻柒。

再看看讀取操作:

    @Nullable
    public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            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();
        }
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
    }

讀取也是同步的扛拨,先加個(gè)鎖,在里面用while不斷判斷是否加載完成举塔,沒(méi)有就釋放鎖绑警,到它繼續(xù)執(zhí)行的時(shí)候,又會(huì)再判斷是否完成央渣。

如此计盒,SharedPreferences就解析完了。

小結(jié)

  • 它采用通過(guò)XmlParser將文件以Map的形式加載到內(nèi)存中的方式芽丹,避免頻繁文件讀寫(xiě)北启。
  • 它將修改保存在一個(gè)臨時(shí)的Map中,commit/apply的時(shí)候拔第,再統(tǒng)一提交到內(nèi)存和文件中咕村。
  • commit操作會(huì)阻塞當(dāng)前線程,不需要等待SharedPreferences操作結(jié)果的話蚊俺,使用apply()效率更高
  • 第一次使用SharedPreferences時(shí)懈涛,它有一個(gè)異步讀取xml文件到內(nèi)存的過(guò)程,所以立即讀取值的話會(huì)失敗泳猬。

關(guān)于線程同步批钠,可以參考:

java并發(fā)之原子性宇植、可見(jiàn)性、有序性
volatile你了解多少埋心?
深入淺出Synchronized
synchronized(this)指郁、synchronized(class)與synchronized(Object)的區(qū)別

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市拷呆,隨后出現(xiàn)的幾起案子坡氯,更是在濱河造成了極大的恐慌,老刑警劉巖洋腮,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件箫柳,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡啥供,警方通過(guò)查閱死者的電腦和手機(jī)悯恍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)伙狐,“玉大人涮毫,你說(shuō)我怎么就攤上這事〈海” “怎么了罢防?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)唉侄。 經(jīng)常有香客問(wèn)我咒吐,道長(zhǎng),這世上最難降的妖魔是什么属划? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任恬叹,我火速辦了婚禮,結(jié)果婚禮上同眯,老公的妹妹穿的比我還像新娘绽昼。我一直安慰自己,他們只是感情好须蜗,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布硅确。 她就那樣靜靜地躺著,像睡著了一般明肮。 火紅的嫁衣襯著肌膚如雪菱农。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,741評(píng)論 1 289
  • 那天晤愧,我揣著相機(jī)與錄音大莫,去河邊找鬼蛉腌。 笑死官份,一個(gè)胖子當(dāng)著我的面吹牛只厘,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播舅巷,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼羔味,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了钠右?” 一聲冷哼從身側(cè)響起赋元,我...
    開(kāi)封第一講書(shū)人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎飒房,沒(méi)想到半個(gè)月后搁凸,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評(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,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡嫡良,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出献酗,到底是詐尸還是另有隱情寝受,我是刑警寧澤,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布罕偎,位于F島的核電站很澄,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏颜及。R本人自食惡果不足惜痴怨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望器予。 院中可真熱鬧浪藻,春花似錦、人聲如沸乾翔。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)反浓。三九已至萌丈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間雷则,已是汗流浹背辆雾。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留月劈,地道東北人度迂。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓藤乙,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親惭墓。 傳聞我的和親對(duì)象是個(gè)殘疾皇子坛梁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

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