Android 數據存儲知識梳理(3) SharedPreference 源碼解析

1、概述

SharedPreferences在開發當中常被用做保存一些相似於配置項這類輕量級的數據,它採用鍵值對的格式,將數據存儲在xml文件當中,並保存在data/data/{應用包名}/shared_prefs下: bash

今天咱們就來一塊兒研究一下 SP的實現原理。

2、SP 源碼解析

2.1 獲取 SharedPreferences 對象

在經過SP進行讀寫操做時,首先須要得到一個SharedPreferences對象,SharedPreferences是一個接口,它定義了系列讀寫的接口,其實現類爲SharedPreferencesImpl、在實際過程當中,咱們通常經過Application、Activity、Service的下面這個方法來獲取SP對象:app

public SharedPreferences getSharedPreferences(String name, int mode)
複製代碼

來獲取SharedPreferences實例,而它們最終都是調用到ContextImplgetSharedPreferences方法,下面是整個調用的結構: 異步

ContextImpl當中, SharedPreferences是以一個靜態雙重 ArrayMap的結構來保存的:

private static ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>> sSharedPrefs;
複製代碼

下面,咱們看一下獲取SP實例的過程:函數

public SharedPreferences getSharedPreferences(String name, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            if (sSharedPrefs == null) {
                sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
            }
            //1.第一個維度是包名.
            final String packageName = getPackageName();
            ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
            if (packagePrefs == null) {
                packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
                sSharedPrefs.put(packageName, packagePrefs);
            }
            //2.第二個維度就是調用get方法時傳入的name,而且若是已經存在了那麼直接返回
            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的構造傳入了一個和name相關聯的File,它就是咱們在第一節當中所說的xml文件,在構造函數中,會去預先讀取這個xml文件當中的內容:post

SharedPreferencesImpl(File file, int mode) {
        //..
        startLoadFromDisk(); //讀取xml文件的內容
}
複製代碼

這裏啓動了一個異步的線程,須要注意的是這裏會將標誌位mLoad置爲false,後面咱們會談到這個標誌的做用:ui

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

loadFromDiskLocked中,將xml文件中的內容保存到Map當中,在讀取完畢以後,喚醒以前有可能阻塞的讀寫線程:this

private Map<String, Object> mMap;

    private void loadFromDiskLocked() {
        //1.若是已經在加載,那麼返回.
        if (mLoaded) {
            return;
        }

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

        //...
        //3.因爲讀寫操做只有在mLoaded變量爲true時纔可進行,所以它們有可能阻塞在調用讀寫操做的方法上,所以這裏須要喚醒它們。
        notifyAll();
    }
複製代碼

SP對象的獲取過程來看,咱們能夠得出下面幾個結論:spa

  • 與某個name所對應的SP對象須要等到調用getSharedPreferences纔會被建立
  • 對於同一進程而言,在Activity/Application/Service獲取SP對象時,若是name相同,它們實際上獲取到的是同一個SP對象
  • 因爲使用的是靜態容器來保存,所以即便Activity/Service銷燬了,它以前建立的SP對象也不會被釋放,而SP中的數據又是用Map來保存的,也就是說,咱們只要調用了某個name相關聯的getSharedPreferences方法,那麼和該name對應的xml文件中的數據都會被讀到內存當中,而且一直到進程被結束。

2.2 經過 SharedPreferences 進行讀取操做

讀取的操做很簡單,它其實就是從之間預先讀取的mMap當中去取出對應的數據,以getBoolean爲例:線程

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

這裏惟一須要關心的是awaitLoadedLocked方法:code

private void awaitLoadedLocked() {
        //這裏若是判斷沒有加載完畢,那麼會進入無限等待狀態
        while (!mLoaded) {
            try {
                wait();
            } catch (InterruptedException unused) {}
        }
    }
複製代碼

在這個方法中,會去檢查mLoaded標誌位是否爲true,若是不爲true,那麼說明沒有加載完畢,該線程會釋放它所持有的鎖,進入等待狀態,直到loadFromDiskLocked加載完xml文件中的內容調用notifyAll()後,該線程才被喚醒。

從讀取操做來看,咱們能夠得出如下兩個結論:

  • 任什麼時候刻讀取操做,讀取的都是內存中的值,而並非xml文件的值。
  • 在調用讀取方法時,若是構造函數中的預讀取線程沒有執行完畢,那麼將會致使讀取的線程進入等待狀態。

2.3 經過 SharedPreferences 進行寫入操做

2.3.1 獲取 EditorImpl

當咱們須要經過SharedPreferences寫入信息時,那麼首先須要經過.edit()得到一個Editor對象,這裏和讀取操做相似,都是須要等到預加載的線程執行完畢:

public Editor edit() {
        synchronized (this) {
            awaitLoadedLocked();
        }
        return new EditorImpl();
    }
複製代碼

Editor的實現類爲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;
            }
        }
   }
複製代碼

由上面的代碼能夠看出,當咱們調用EditorputXXX方法時,實際上並無保存到SPmMap當中,而僅僅是保存到經過.edit()返回的EditorImpl的臨時變量當中。

2.3.2 apply 和 commit 方法

咱們經過editor寫入的數據,最終須要等到調用editorapplycommit方法,纔會寫入到內存和xml這兩個地方。

(a) apply

下面,咱們先看比較經常使用的apply方法:

public void apply() {
            //1.將修改操做提交到內存當中.
            final MemoryCommitResult mcr = commitToMemory();
           
            //2.寫入文件當中
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); //postWriteRunnable在寫入文件完成後進行一些收尾操做.
            
            //3.只要寫入到內存當中,就通知監聽者.
            notifyListeners(mcr);
        }
複製代碼

整個apply分爲三個步驟:

  • 經過commitToMemory寫入到內存中
  • 經過enqueueDiskWrite寫入到磁盤中
  • 通知監聽者

其中第一個步驟很好理解,就是根據editor中的內容,肯定哪些是須要更新的數據,而後把SP當中的mMap變量進行更新,以後將變化的內容封裝成MemoryCommitResult結構體。

咱們主要看一下第二步,是如何寫入磁盤當中的:

private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        //1.寫入磁盤任務的runnable.
        final Runnable writeToDiskRunnable = new Runnable() {
                public void run() {
                    //1.1 寫入磁盤
                    synchronized (mWritingToDiskLock) {
                        writeToFile(mcr);
                    }
                    //....執行收尾操做.
                    if (postWriteRunnable != null) {
                        postWriteRunnable.run();
                    }
                }
            };
        
        //2.這裏若是是經過apply方法調用過來的,那麼爲false
        final boolean isFromSyncCommit = (postWriteRunnable == null);

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

        QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
    }
複製代碼

能夠看出,若是調用apply方法,那麼對於xml文件的寫入是在異步線程當中進行的。

(b) commit

若是調用的commit方法,那麼執行的是以下操做:

public boolean commit() {
            //1.寫入內存
            MemoryCommitResult mcr = commitToMemory();
            //2.寫入文件
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null); //因爲是同步進行,因此把收尾操做放到Runnable當中.
            //在這裏執行收尾操做..
            //3.通知監聽
            notifyListeners(mcr);
            return mcr.writeToDiskResult;
        }
複製代碼

當使用commit方法時,和apply相似,都是三步操做,只不過第二步在寫入文件的時候,傳入的Runnablenull,所以,對於寫入文件的操做是同步的,所以,若是咱們在主線程當中調用了commit方法,那麼其實是在主線程進行IO操做。

(c) 回調時機

  • 對於apply方法,因爲它對於文件的寫入是異步的,可是notifyListener方法不會等到真正寫入完成時才通知監聽者,所以監聽者在收到回調或者apply返回時,對於SP數據的改變只是寫入到了內存當中,並無寫入到文件當中。
  • 對於commit方法,因爲它對於文件的寫入是同步的,所以能夠保證監聽者收到回調時或者commit方法返回後,改變已經被寫入到了文件當中。

2.4 監聽 SP 的變化

若是但願監聽SP的變化,那麼能夠經過下面的這兩個方法:

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

    public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
        synchronized(this) {
            mListeners.remove(listener);
        }
    }
複製代碼

因爲對應於NameSP在進程中是其實是一個單例模式,所以,咱們能夠作到在進程中的任何地方改變SP的數據,都能收到監聽。

相關文章
相關標籤/搜索