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

一、概述

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


今天我們就來一起研究一下SP的實現原理。

二、SP 源碼解析

2.1 獲取 SharedPreferences 對象

在通過SP進行讀寫操作時,首先需要獲得一個SharedPreferences對象,SharedPreferences是一個接口,它定義了系列讀寫的接口,其實現類為SharedPreferencesImpl、在實際過程中,我們一般通過Application、Activity、Service的下面這個方法來獲取SP對象:

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文件當中的內容:

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

這里啟動了一個異步的線程,需要注意的是這里會將標志位mLoad置為false,后面我們會談到這個標志的作用:

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

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

    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對象的獲取過程來看,我們可以得出下面幾個結論:

  • 與某個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方法:

    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的數據,都能收到監聽。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容