1. 程式人生 > >Android應用Preference相關及原始碼淺析(SharePreferences篇)

Android應用Preference相關及原始碼淺析(SharePreferences篇)

1 前言

在我們開發Android過程中資料的儲存會有很多種解決方案,譬如常見的檔案儲存、資料庫儲存、網路雲端儲存等,但是Android系統為咱們提供了更加方便的一種資料儲存方式,那就是SharePreference資料儲存。其實質也就是檔案儲存,只不過是符合XML標準的檔案儲存而已,而且其也是Android中比較常用的簡易型資料儲存解決方案。

我們在這裡不僅要探討SharePreference如何使用,還要探討其原始碼是如何實現的;同時還要在下一篇部落格討論由SharePreference衍生出來的Preference相關Android元件實現,不過有意思的是我前幾天在網上看見有人對google的Preference有很大爭議,有人說他就是雞肋,醜而不靈活自定義,有人說他是一個標準,很符合設計思想,至於誰說的有道理,我想看完本文和下一篇文章你自然會有自己的觀點看法的,還有一點就是關於使用SharePreference耗時問題也是一個爭議,分析完再說吧,那就現在開始分析吧(基於API 22原始碼)。

這裡寫圖片描述

2 SharePreferences基本使用例項

在Android提供的幾種資料儲存方式中SharePreference屬於輕量級的鍵值儲存方式,以XML檔案方式儲存資料,通常用來儲存一些使用者行為開關狀態等,也就是說SharePreference一般的儲存型別都是一些常見的資料型別(PS:當然也可以儲存一些複雜物件,不過需要曲線救國,下面會給出儲存複雜物件的解決方案的)。

在我們平時應用開發時或多或少都會用到SharePreference,這裡就先給出一個常見的使用例項,具體如下:

public class MainActivity extends ActionBarActivity
{
private SharedPreferences mSharedPreferences; private SharedPreferences mSharedPreferencesContext; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initTest(); } private
void initTest() { mSharedPreferencesContext = getSharedPreferences("Test", MODE_PRIVATE); mSharedPreferences = getPreferences(MODE_PRIVATE); SharedPreferences.Editor editor = mSharedPreferencesContext.edit(); editor.putBoolean("saveed", true); Set<String> set = new HashSet<>(); set.add("aaaaa"); set.add("bbbbbbbb"); editor.putStringSet("content", set); editor.commit(); SharedPreferences.Editor editorActivity = mSharedPreferences.edit(); editorActivity.putString("name", "haha"); editorActivity.commit(); } }

執行之後adb進入data應用包下的shared_prefs目錄可以看見如下結果:

-rw-rw---- u0_a84   u0_a84        108 2015-08-23 10:34 MainActivity.xml
-rw-rw---- u0_a84   u0_a84        214 2015-08-23 10:34 Test.xml

其內容分別如下:

at Test.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <boolean name="saveed" value="true" />
    <set name="content">
        <string>aaaaa</string>
        <string>bbbbbbbb</string>
    </set>
</map>

at MainActivity.xml
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="name">haha</string>
</map>

可以看見SharePreference的使用還是非常簡單easy的,所以不做太多的使用說明,我們接下來重點依然是關注其實現原理。

3 SharePreferences原始碼分析

3-1 從SharePreferences介面說起

其實講句實話,SharePreference的原始碼沒啥深奧的東東,其實質和ACache類似,都算時比較獨立的東東。分析之前我們還是先來看下SharePreference這個類的原始碼,具體如下:

//你會發現SharedPreferences其實是一個介面而已
public interface SharedPreferences {
    //定義一個用於在資料發生改變時呼叫的監聽回撥
    public interface OnSharedPreferenceChangeListener {
        //哪個key對應的值發生變化
        void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
    }

    //編輯SharedPreferences物件設定值的介面
    public interface Editor {
        //一些編輯儲存基本資料key-value的介面方法
        Editor putString(String key, String value);
        Editor putStringSet(String key, Set<String> values);
        Editor putInt(String key, int value);
        Editor putLong(String key, long value);
        Editor putFloat(String key, float value);
        Editor putBoolean(String key, boolean value);
        //刪除指定key的鍵值對
        Editor remove(String key);
        //清空所有鍵值對
        Editor clear();
        //同步的提交到硬體磁碟
        boolean commit();
        //將修改資料原子提交到記憶體,而後非同步提交到硬體磁碟
        void apply();
    }

    //獲取指定資料
    Map<String, ?> getAll();
    String getString(String key, String defValue);
    Set<String> getStringSet(String key, Set<String> defValues);
    int getInt(String key, int defValue);
    long getLong(String key, long defValue);
    float getFloat(String key, float defValue);
    boolean getBoolean(String key, boolean defValue);
    boolean contains(String key);

    //針對preferences建立一個新的Editor物件,通過它你可以修改preferences裡的資料,並且原子化的將這些資料提交回SharedPreferences物件
    Editor edit();
    //註冊一個回撥函式,當一個preference發生變化時呼叫
    void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
    //登出一個之前(註冊)的回撥函式
    void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
}

很明顯的可以看見,SharePreference原始碼其實是很簡單的。既然這裡說了SharePreference類只是一個介面,那麼他一定有自己的實現類的,怎麼辦呢?我們繼續往下看。

3-2 SharePreferences實現類SharePreferencesImpl分析

我們從上面SharePreference的使用入口可以分析,具體可以知道SharePreference的例項獲取可以通過兩種方式獲取,一種是Activity的getPreferences方法,一種是Context的getSharedPreferences方法。所以我們如下先來看下這兩個方法的原始碼。

先來看下Activity的getPreferences方法原始碼,如下:

    public SharedPreferences getPreferences(int mode) {
        return getSharedPreferences(getLocalClassName(), mode);
    }

哎?可以發現,其實Activity的SharePreference例項獲取方法只是對Context的getSharedPreferences再一次封裝而已,使用getPreferences方法獲取例項預設生成的xml檔名字是當前activity類名而已。既然這樣那我們還是轉戰Context(其實現在ContextImpl中,至於不清楚Context與ContextImpl及Activity關係的請先看這篇博文,點我迅速腦補)的getSharedPreferences方法,具體如下:

//ContextImpl類中的靜態Map宣告,全域性的一個sSharedPrefs
private static ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>> sSharedPrefs;

//獲取SharedPreferences例項物件
public SharedPreferences getSharedPreferences(String name, int mode) {
    //SharedPreferences的實現類物件引用宣告
    SharedPreferencesImpl sp;
    //通過ContextImpl保證同步操作
    synchronized (ContextImpl.class) {
        if (sSharedPrefs == null) {
            //例項化物件為一個複合Map,key-package,value-map
            sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
        }
    //獲取當前應用包名
        final String packageName = getPackageName();
        //通過包名找到與之關聯的prefs集合packagePrefs
        ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
        //懶漢模式例項化
        if (packagePrefs == null) {
            //如果沒找到就new一個包的prefs,其實就是一個檔名對應一個SharedPreferencesImpl,可以有多個對應,所以用map
            packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
            //以包名為key,例項化的所有檔案map作為value新增到sSharedPrefs
            sSharedPrefs.put(packageName, packagePrefs);
        }

        if (mPackageInfo.getApplicationInfo().targetSdkVersion <
                Build.VERSION_CODES.KITKAT) {
            if (name == null) {
                //nice處理,name傳null時用"null"代替
                name = "null";
            }
        }
    //找出與檔名name關聯的sp物件
        sp = packagePrefs.get(name);
        if (sp == null) {
            //如果沒找到則先根據name構建一個File的prefsFile物件
            File prefsFile = getSharedPrefsFile(name);
            //依據上面的File物件建立一個SharedPreferencesImpl物件的例項
            sp = new SharedPreferencesImpl(prefsFile, mode);
            //以key-value方式新增到packagePrefs中
            packagePrefs.put(name, sp);
            返回與name相關的SharedPreferencesImpl物件
            return sp;
        }
    }
    //如果不是第一次,則在3.0之前(預設具備該mode)或者mode為MULTI_PROCESS時呼叫reload方法
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
            getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        //重新載入檔案資料
        sp.startReloadIfChangedUnexpectedly();
    }
    //返回SharedPreferences例項物件sp
    return sp;
}

我們可以發現,上面方法中首先調運了getSharedPrefsFile來獲取一個File物件,所以我們繼續先來看下這個方法,具體如下:

    public File getSharedPrefsFile(String name) {
        //依據我們傳入的檔名字串建立一個字尾為xml的檔案
        return makeFilename(getPreferencesDir(), name + ".xml");
    }

    private File getPreferencesDir() {
        synchronized (mSync) {
            if (mPreferencesDir == null) {
                //獲取當前app的data目錄下的shared_prefs目錄
                mPreferencesDir = new File(getDataDirFile(), "shared_prefs");
            }
            return mPreferencesDir;
        }
    }

可以看見,原來SharePreference檔案儲存路徑和檔案建立是這個來的。繼續往下看可以發現接著調運了SharedPreferencesImpl的建構函式,至於這個建構函式用來幹嘛,下面會分析。

好了,到這裡我們先回過頭稍微總結一下目前的原始碼分析結論,具體如下:

前面我們有文章分析了Android中的Context,這裡又發現ContextImpl中有一個靜態的ArrayMap變數sSharedPrefs。這時候你想到了啥呢?無論有多少個ContextImpl物件例項,系統都共享這一個sSharedPrefs的Map,應用啟動以後首次使用SharePreference時建立,系統結束時才可能會被垃圾回收器回收,所以如果我們一個App中頻繁的使用不同檔名的SharedPreferences很多時這個Map就會很大,也即會佔用移動裝置寶貴的記憶體空間,所以說我們應用中應該儘可能少的使用不同檔名的SharedPreferences,取而代之的是合併他們,減小記憶體使用。同時上面最後一段程式碼也及具有隱藏含義,其表明了SharedPreferences是可以通過MODE_MULTI_PROCESS來進行誇程序訪問檔案資料的,其reload就是為了誇程序能更好的重新整理訪問資料。

好了,還記不記得上面我們分析留的尾巴呢?現在我們就來看看這個尾巴,可以發現SharedPreferencesImpl類其實就是SharedPreferences介面的實現類,其建構函式如下:

final class SharedPreferencesImpl implements SharedPreferences {
    ......
    //建構函式,file是前面分析data目錄下建立的傳入name的xml檔案,mode為傳入的訪問方式
    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        //依據檔名建立一個同名的.bak備份檔案,當mFile出現crash的會用mBackupFile來替換恢復資料
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        //將檔案從flash或者sdcard非同步載入到記憶體中
        startLoadFromDisk();
    }
    ......
    //建立同名備份檔案
    private static File makeBackupFile(File prefsFile) {
        return new File(prefsFile.getPath() + ".bak");
    }
    ......
    private void startLoadFromDisk() {
        //同步操作mLoaded標誌,寫為未載入,這貨是關鍵的關鍵!!!!
        synchronized (this) {
            mLoaded = false;
        }
        //開啟一個執行緒非同步同步載入disk檔案到記憶體
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                synchronized (SharedPreferencesImpl.this) {
                    //新執行緒中在SharedPreferencesImpl物件鎖中非同步load資料,如果此時資料還未load完成,則其他執行緒呼叫SharedPreferences.getXXX方法都會被阻塞,具體原因關注mLoaded標誌變數即可!!!!!
                    loadFromDiskLocked();
                }
            }
        }.start();
    }
}

好了,到這裡你會發現整個SharedPreferencesImpl的建構函式很簡單,那我們就繼續分析真正的非同步載入檔案到記憶體過程,如下:

    private void loadFromDiskLocked() {
        //如果已經非同步載入直接return返回
        if (mLoaded) {
            return;
        }
        //如果存在備份檔案則直接使用備份檔案
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
        ......
        Map map = null;
        StructStat stat = null;
        try {
            //獲取Linux檔案stat資訊,Linux高階C中經常出現的
            stat = Os.stat(mFile.getPath());
            //檔案至少是可讀的
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try {
                    //把檔案以BufferedInputStream流讀出來
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16*1024);
                    //使用系統提供的XmlUtils工具類將xml流解析轉換為map型別資料
                    map = XmlUtils.readMapXml(str);
                } catch (XmlPullParserException e) {
                    Log.w(TAG, "getSharedPreferences", e);
                } catch (FileNotFoundException e) {
                    Log.w(TAG, "getSharedPreferences", e);
                } catch (IOException e) {
                    Log.w(TAG, "getSharedPreferences", e);
                } finally {
                    IoUtils.closeQuietly(str);
                }
            }
        } catch (ErrnoException e) {
        }
        //標記置為為已讀
        mLoaded = true;
        if (map != null) {
            //把解析的map賦值給mMap
            mMap = map;
            mStatTimestamp = stat.st_mtime;//記錄時間戳
            mStatSize = stat.st_size;//記錄檔案大小
        } else {
            mMap = new HashMap<String, Object>();
        }
        //喚醒其他等待執行緒(其實就是調運該類的getXXX方法的執行緒),因為在getXXX時會通過mLoaded標記是否進入wait,所以這裡需要notify
        notifyAll();
    }

OK,到此整個Android應用獲取SharePreference例項的過程我們就分析完了,簡單總結下如下:

建立相關許可權和mode的xml檔案,非同步同步鎖載入xml檔案並解析xml資料為map型別到記憶體中等待使用操作,特別注意,在xml檔案非同步載入未完成時調運SharePreference的getXXX及setXXX方法是阻塞等待的。由此也可以知道,一旦拿到SharePreference物件之後的getXXX操作其實都不再是檔案讀操作了,也就不存在網上扯蛋的認為多次頻繁使用getXXX方法降低效能的說法了。

分析完了構造例項化,我們回憶可以知道使用SharePreference可以通過getXXX方法直接獲取已經存在的key-value資料,下面我們就來看下這個過程,這裡我們隨意看一個方法即可,如下:

    public boolean getBoolean(String key, boolean defValue) {
        //可以看見,和上面非同步load資料使用的是同一個物件鎖
        synchronized (this) {
            //阻塞等待非同步載入執行緒載入完成notify
            awaitLoadedLocked();
            //載入完成後解析的xml資料放在mMap物件中,我們從mMap中找出指定key的資料
            Boolean v = (Boolean)mMap.get(key);
            //存在返回找到的值,不存在返回設定的defValue
            return v != null ? v : defValue;
        }
    }

先不解釋,我們來關注下上面方法調運的awaitLoadedLocked方法,具體如下:

    private void awaitLoadedLocked() {
        ......
        //核心,這就是非同步阻塞等待
        while (!mLoaded) {
            try {
                wait();
            } catch (InterruptedException unused) {
            }
        }
    }

哈哈,不解釋,這也太赤裸裸的明顯了,就是阻塞,就是這麼任性,沒轍。那我們繼續攻佔高地唄,get完事了,那就是set了呀。

3-3 SharePreferences內部類Editor實現EditorImpl分析

還記不記得set是在SharePreference介面的Editor介面中定義的,而SharePreference提供了edit()方法來獲取Editor例項,我們先來看下這個edit()方法吧,如下:

    public Editor edit() {
        //握草!這也和非同步load用的一把鎖
        synchronized (this) {
            //阻塞等待,不解釋吧,向上看。。。
            awaitLoadedLocked();
        }
    //非同步載入OK以後通過EditorImpl建立Editor例項
        return new EditorImpl();
    }

可以看見,SharePreference的edit()方法其實就是阻塞等待返回一個Editor的例項(Editor的實現是EditorImpl),那我們就順藤摸瓜一把,來看下這個EditorImpl這個類,如下:

    public final class EditorImpl implements Editor {
        //建立一個mModified的key-value集合,用來在記憶體中暫存資料
        private final Map<String, Object> mModified = Maps.newHashMap();
        //一個是否清除preference的flag
        private boolean mClear = false;

        ......//省略類似的putXXX方法
        public Editor putBoolean(String key, boolean value) {
            //同步鎖操作
            synchronized (this) {
                //將我們要儲存的資料放入mModified集合中
                mModified.put(key, value);
                //返回當前物件例項,方便這種模式的程式碼寫法:putXXX().putXXX();
                return this;
            }
        }
        //不用過多解釋,同步刪除mModified中包含key的資料
        public Editor remove(String key) {
            synchronized (this) {
                mModified.put(key, this);
                return this;
            }
        }
        //不解釋,要清楚所有資料則直接置位mClear標記
        public Editor clear() {
            synchronized (this) {
                mClear = true;
                return this;
            }
        }
        ......
    }

好了,到此你可以發現Editor的setXXX及clear操作僅僅只是將相關資料暫存到記憶體中或者設定好標記為,也就是說調運了Editor的putXXX後其實資料是沒有存入SharePreference的。那麼通過我們一開始的例項可以知道,要想將Editor的資料存入SharePreference檔案需要調運Editor的commit或者apply方法來生效。所以我們接下來先來看看Editor類常用的commit方法實現原理,如下:

public boolean commit() {
    //1.先通過commitToMemory方法提交到記憶體
    MemoryCommitResult mcr = commitToMemory();
    //2.寫檔案操作
    SharedPreferencesImpl.this.enqueueDiskWrite(
            mcr, null /* sync write on this thread okay */);
    try {
        //阻塞等待寫操作完成,UI操作需要注意!!!所以如果不關心返回值可以考慮用apply替代,具體原因等會分析apply就明白了。
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    }
    //3.通知資料發生變化了
    notifyListeners(mcr);
    //4.返回寫檔案是否成功狀態
    return mcr.writeToDiskResult;
}

我去,小小一個commit方法做了這麼多操作,主要分為四個步驟,我們先來看下第一個步驟,通過commitToMemory方法提交到記憶體返回一個MemoryCommitResult物件。分析commitToMemory方法前先看下MemoryCommitResult這個類,具體如下:

// Return value from EditorImpl#commitToMemory()
//也是內部類,只是為了組織資料結構而誕生,也就是EditorImpl.commitToMemory()的返回值
private static class MemoryCommitResult {
    public boolean changesMade;  // any keys different?
    public List<String> keysModified;  // may be null
    public Set<android.content.SharedPreferences.OnSharedPreferenceChangeListener> listeners;  // may be null
    public Map<?, ?> mapToWriteToDisk;
    public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
    public volatile boolean writeToDiskResult = false;

    public void setDiskWriteResult(boolean result) {
        writeToDiskResult = result;
        writtenToDiskLatch.countDown();
    }
}

回過頭現在來看commitToMemory方法,具體如下:

// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
    //啥也不說,先整一個例項化物件
    MemoryCommitResult mcr = new MemoryCommitResult();
    //和SharedPreferencesImpl共用一把鎖
    synchronized (SharedPreferencesImpl.this) {
        // We optimistically don't make a deep copy until
        // a memory commit comes in when we're already
        // writing to disk.
        if (mDiskWritesInFlight > 0) {
            // We can't modify our mMap as a currently
            // in-flight write owns it.  Clone it before
            // modifying it.
            // noinspection unchecked
            //有多個未完成的寫操作時複製一份,但是我們不知道用來幹啥???????
            mMap = new HashMap<String, Object>(mMap);
        }
        //構造資料結構,把通過SharedPreferencesImpl建構函式裡非同步載入的檔案xml解析結果mMap賦值給要寫到disk的Map
        mcr.mapToWriteToDisk = mMap;
        //增加一個未完成的寫opt
        mDiskWritesInFlight++;
    //判斷有沒有監聽設定
        boolean hasListeners = mListeners.size() > 0;
        if (hasListeners) {
            //建立監聽佇列
            mcr.keysModified = new ArrayList<String>();
            mcr.listeners =
                    new HashSet<android.content.SharedPreferences.OnSharedPreferenceChangeListener>(mListeners.keySet());
        }
        //再加一把自己的鎖
        synchronized (this) {
            //如果調運的是Editor的clear方法,則這裡commit時這麼處理
            if (mClear) {
                //如果從檔案里加載出來的xml不為空
                if (!mMap.isEmpty()) {
                    //設定資料結構中資料變化標誌為true
                    mcr.changesMade = true;
                    //清空記憶體中xml資料
                    mMap.clear();
                }
                //處理完畢,標記復位,程式繼續執行,所以如果這次Editor中如果有寫資料且還未commit,則執行完這次commit之後不會清掉本次寫操作的資料,只會clear以前xml檔案中的所有資料
                mClear = false;
            }
        //mModified是調運Editor的setXXX零時儲存的map
            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.
                //刪除需要刪除的key-value
                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;
                        }
                    }
                    //把變化和新加的資料更新到SharePreferenceImpl的mMap中
                    mMap.put(k, v);
                }
            //設定資料結構變化標記
                mcr.changesMade = true;
                if (hasListeners) {
                    //設定監聽
                    mcr.keysModified.add(k);
                }
            }
            //清空Editor中零時儲存的資料
            mModified.clear();
        }
    }
    //返回重新更新過mMap值封裝的資料結構
    return mcr;
}   

到此我們Editor的commit方法的第一步已經完成,根據寫操作組織記憶體資料,返回組織後的資料結構。接下來我們繼續回到commit方法看下第二步—-寫到檔案中,其核心是調運SharedPreferencesImpl類的enqueueDiskWrite方法實現。具體如下:

//按照佇列把記憶體資料寫入磁碟,commit時postWriteRunnable為null,apply時不為null
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    //建立一個writeToDiskRunnable的Runnable物件
    final Runnable writeToDiskRunnable = new Runnable() {
        public void run() {
            synchronized (mWritingToDiskLock) {
                //真正的寫檔案操作
                writeToFile(mcr);
            }
            synchronized (SharedPreferencesImpl.this) {
                //寫完一個計數器-1
                mDiskWritesInFlight--;
            }
            if (postWriteRunnable != null) {
                //等會apply分析
                postWriteRunnable.run();
            }
        }
    };
    //判斷是同步寫還是非同步
    final boolean isFromSyncCommit = (postWriteRunnable == null);

    // Typical #commit() path with fewer allocations, doing a write on
    // the current thread.
    //commit方式走這裡
    if (isFromSyncCommit) {
        boolean wasEmpty = false;
        synchronized (SharedPreferencesImpl.this) {
            //如果當前只有一個寫操作
            wasEmpty = mDiskWritesInFlight == 1;
        }
        if (wasEmpty) {
            //一個寫操作就直接在當前執行緒中寫檔案,不用另起執行緒
            writeToDiskRunnable.run();
            //寫完檔案就返回
            return;
        }
    }
    //如果是apply就線上程池中執行
    QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

可以發現,commit從記憶體寫檔案是在當前調運執行緒中直接執行的。那我們再來看看這個寫記憶體到磁碟方法中真正的寫方法writeToFile,如下:

    // Note: must hold mWritingToDiskLock
    private void writeToFile(MemoryCommitResult mcr) {
        if (mFile.exists()) {
            if (!mcr.changesMade) {
                //如果檔案存在且沒有改變的資料則直接返回寫OK
                mcr.setDiskWriteResult(true);
                return;
            }

            if (!mBackupFile.exists()) {
                //如果要寫入的檔案已經存在,並且備份檔案不存在時就先把當前檔案備份一份,因為如果本次寫操作失敗時資料可能已經亂了,所以下次例項化load資料時可以從備份檔案中恢復
                if (!mFile.renameTo(mBackupFile)) {
                    Log.e(TAG, "Couldn't rename file " + mFile
                          + " to backup file " + mBackupFile);
                    //命名失敗直接返回寫失敗了
                    mcr.setDiskWriteResult(false);
                    return;
                }
            } else {
                //備份檔案存在就把原始檔刪掉,因為要寫新的
                mFile.delete();
            }
        }

        try {
            //建立mFile檔案
            FileOutputStream str = createFileOutputStream(mFile);
            if (str == null) {
                //建立失敗直接返回寫失敗了
                mcr.setDiskWriteResult(false);
                return;
            }
            //把資料寫入mFile檔案
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
            //徹底同步到磁碟檔案中
            FileUtils.sync(str);
            str.close();
            //設定檔案許可權mode            
            ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
            //和剛開始例項化load時一樣,更新檔案時間戳和大小
            try {
                final StructStat stat = Os.stat(mFile.getPath());
                synchronized (this) {
                    mStatTimestamp = stat.st_mtime;
                    mStatSize = stat.st_size;
                }
            } catch (ErrnoException e) {
                // Do nothing
            }
            // Writing was successful, delete the backup file if there is one.
            //寫成功了mFile那就把備份檔案直接刪掉,沒用了。
            mBackupFile.delete();
            //設定寫成功了,然後返回
            mcr.setDiskWriteResult(true);
            return;
        } catch (XmlPullParserException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        } catch (IOException e) {
            Log.w(TAG, "writeToFile: Got exception:", e);
        }
        // Clean up an unsuccessfully written file
        if (mFile.exists()) {
            //上面如果出錯了就刪掉,因為寫之前已經備份過資料了,下次load時load備份資料
            if (!mFile.delete()) {
                Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
            }
        }
        //寫失敗了
        mcr.setDiskWriteResult(false);
    }

回過頭可以發現,上面commit的第二步寫磁碟操作其實是做了類似資料庫的事務操作機制的(備份檔案)。接著可以繼續分析commit方法的第三四步,很明顯可以看出,第三步就是回撥設定的監聽方法,通知資料變化了,第四步就是返回commit寫檔案是否成功。

總體到這裡你可以發現,一個常用的SharePreferences過程已經完全分析完畢。接下來我們就再簡單說說Editor的apply方法原理,先來看下Editor的apply方法,如下:

public void apply() {
    //有了上面commit分析,這個雷同,寫資料到記憶體,返回資料結構
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
        public void run() {
            try {
                //等待寫檔案結束
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException ignored) {
            }
        }
    };

    QueuedWork.add(awaitCommit);
    //一個收尾的Runnable
    Runnable postWriteRunnable = new Runnable() {
        public void run() {
            awaitCommit.run();
            QueuedWork.remove(awaitCommit);
        }
    };
    //這個上面commit已經分析過的,這裡postWriteRunnable不為null,所以會在一個新的執行緒池調運postWriteRunnable的run方法
    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);
}

看到了吧,其實和commit類似,只不過他是非同步寫的,沒在當前執行緒執行寫檔案操作,還有就是他不像commit一樣返回檔案是否寫成功狀態。

3-4 SharePreferences原始碼分析總結

題外趣事: 記得好像是去年有一次我用SharePreferences儲存了幾個boolean值,由於開發除錯,當時我直接進入系統data目錄下應用的xml儲存資料夾,然後執行了刪除操作;接著我沒有重啟應用,直接打斷點除錯,握草!奇蹟的發現SharePreferences調運get時竟然拿到了不是初始化的值。哈哈,其實就是上面分析的,載入完後是一個靜態的map,程序沒掛之前一直用的記憶體資料。

通過上面的例項及原始碼分析可以發現:

  • SharedPreferences在例項化時首先會從sdcard非同步讀檔案,然後快取在記憶體中;接下來的讀操作都是記憶體快取操作而不是檔案操作。

  • 在SharedPreferences的Editor中如果用commit()方法提交資料,其過程是先把資料更新到記憶體,然後在當前執行緒中寫檔案操作,提交完成返回提交狀態;如果用的是apply()方法提交資料,首先也是寫到記憶體,接著在一個新執行緒中非同步寫檔案,然後沒有返回值。

  • 由於上面分析了,在寫操作commit時有三級鎖操作,所以效率很低,所以當我們一次有多個修改寫操作時等都批量put完了再一次提交確認,這樣可以提高效率。

可以發現,在簡單資料行為狀態儲存中,Android的SharedPreferences是一個安全而且不錯的選擇。

4 SharePreferences進階專案解決方案

其實分析完原始碼之後也就差不多了。這裡所謂的進階專案解決方案只是曲線救國的2B行為,只是表明還有這麼一種方案,至於專案中是否值得提倡那就要綜合酌情考慮了。

4-1 SharePreferences儲存複雜物件的解決案例

這個案例完全有些多餘(因為看完這個例子你會發現還不如使用github上大神流行的ACache更爽呢!),但是也比較有意思,所以還是拿出來說說,其實網上實現的也很多。

我們有時候可能會涉及到儲存一個自定義物件到SharedPreferences中,這個怎麼實現呢?標準的SharedPreferences的Editor只提供幾個常見型別的put方法呀,其實可以實現的,原理就是Base64轉碼為字串儲存,如下給出我的一個工具類,在專案中可以直接使用:

/**
 * @author 工匠若水
 * @version 1.0
 * http://blog.csdn.net/yanbober
 * 儲存任意型別物件到SharedPreferences工具類
 */
public final class ObjectSharedPreferences {
    private Context mContext;
    private String mName;
    private int mMode;

    public ObjectSharedPreferences(Context context, String name) {
        this(context, name, Context.MODE_PRIVATE);
    }

    public ObjectSharedPreferences(Context context, String name, int mode) {
        this.mContext = context;
        this.mName = name;
        this.mMode = mode;
    }

    /**
     * 儲存任意object物件到SharedPreferences
     * @param key
     * @param object
     */
    public void setObject(String key, Object object) {
        SharedPreferences preferences = mContext.getSharedPreferences(mName, mMode);

        ObjectOutputStream objOutputStream = null;
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try {
            objOutputStream = new ObjectOutputStream(outputStream);
            objOutputStream.writeObject(object);
            String objectVal = new String(Base64.encode(outputStream.toByteArray(), Base64.DEFAULT));
            SharedPreferences.Editor editor = preferences.edit();
            editor.putString(key, objectVal);
            editor.commit();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (outputStream != null) {
                    outputStream.close();
                }
                if (objOutputStream != null) {
                    objOutputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 從SharedPreferences獲取任意object物件
     * @param key
     * @param clazz
     * @return
     */
    public <T> T getObject(String key, Class<T> clazz) {
        SharedPreferences preferences = this.mContext.getSharedPreferences(mName, mMode);
        if (preferences.contains(key)) {
            String objectVal = preferences.getString(key, null);
            byte[] buffer = Base64.decode(objectVal, Base64.DEFAULT);
            ByteArrayInputStream inputStream = new ByteArrayInputStream(buffer);
            ObjectInputStream objInputStream = null;
            try {
                objInputStream = new ObjectInputStream(inputStream);
                return (T) objInputStream.readObject();
            } catch (StreamCorruptedException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (inputStream != null) {
                        inputStream.close();
                    }
                    if (objInputStream != null) {
                        objInputStream.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }
}

好了,沒啥解釋的,依據需求自己決定吧,這只是一種方案而已,其替代方案很多。

4-2 SharePreferences跨程序訪問解決案例

Android系統有自己的一套安全機制,當應用程式在安裝時系統會分配給他們一個唯一的userid,當該應用程式需要訪問檔案等資源的時候必須要匹配userid。預設情況下安卓應用程式建立的各種檔案(SharePreferences、資料庫等)都是私有的(在/data/data/[APP PACKAGE NAME]/files),其他程式無法訪問。在3.0以後必須在建立檔案時顯示的指定了Context.MODE_WORLD_READABLE或者Context.MODE_WORLD_WRITEABLE才能被其他程序(應用)訪問。

這個案例也就類似於Android的ContentProvider,可以跨程序訪問,但是其原理是給許可權然後多程序檔案訪問而已。具體我們看如下一個案例,一個用來類比當服務端,一個用來類比當客戶端,如下:

服務端:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private TextView mTextView;
    private EditText mEditText;
    private Button mButton;

    private SharedPreferences mPreferences;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mTextView = (TextView) findViewById(R.id.content);
        mEditText = (EditText) findViewById(R.id.input);
        mButton = (Button) findViewById(R.id.click);
        mButton.setOnClickListener(this);
        //操作當前APP本地的SharedPreferences檔案local.xml
        mPreferences = getSharedPreferences("local", MODE_WORLD_READABLE | MODE_WORLD_WRITEABLE);
    }

    @Override
    public void onClick(View v) {
        mTextView.setText(mPreferences.getString("input", "default"));
        SharedPreferences.Editor editor = mPreferences.edit();
        editor.putString("input", mEditText.getText().toString());
        editor.commit();
    }
}

客戶端:

public class MainActivity extends AppCompatActiv