SharePreferences相關及源碼淺析

轉(zhuǎn)載自:https://blog.csdn.net/yanbober/article/details/47866369

1 前言

在我們開發(fā)Android過程中數(shù)據(jù)的存儲會有很多種解決方案,譬如常見的文件存儲、數(shù)據(jù)庫存儲、網(wǎng)絡云存儲等,但是Android系統(tǒng)為咱們提供了更加方便的一種數(shù)據(jù)存儲方式,那就是SharePreference數(shù)據(jù)存儲。其實質(zhì)也就是文件存儲,只不過是符合XML標準的文件存儲而已,而且其也是Android中比較常用的簡易型數(shù)據(jù)存儲解決方案。

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

2 SharePreferences基本使用實例

在Android提供的幾種數(shù)據(jù)存儲方式中SharePreference屬于輕量級的鍵值存儲方式,以XML文件方式保存數(shù)據(jù),通常用來存儲一些用戶行為開關狀態(tài)等,也就是說SharePreference一般的存儲類型都是一些常見的數(shù)據(jù)類型(PS:當然也可以存儲一些復雜對象,不過需要曲線救國,下面會給出存儲復雜對象的解決方案的)。

在我們平時應用開發(fā)時或多或少都會用到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目錄可以看見如下結(jié)果:

-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

其內(nèi)容分別如下:

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的,所以不做太多的使用說明,我們接下來重點依然是關注其實現(xiàn)原理。

3 SharePreferences源碼分析

3-1 從SharePreferences接口說起

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

//你會發(fā)現(xiàn)SharedPreferences其實是一個接口而已
public interface SharedPreferences {
    //定義一個用于在數(shù)據(jù)發(fā)生改變時調(diào)用的監(jiān)聽回調(diào)
    public interface OnSharedPreferenceChangeListener {
        //哪個key對應的值發(fā)生變化
        void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
    }

    //編輯SharedPreferences對象設定值的接口
    public interface Editor {
        //一些編輯存儲基本數(shù)據(jù)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();
        //將修改數(shù)據(jù)原子提交到內(nèi)存,而后異步提交到硬件磁盤
        void apply();
    }

    //獲取指定數(shù)據(jù)
    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創(chuàng)建一個新的Editor對象,通過它你可以修改preferences里的數(shù)據(jù),并且原子化的將這些數(shù)據(jù)提交回SharedPreferences對象
    Editor edit();
    //注冊一個回調(diào)函數(shù),當一個preference發(fā)生變化時調(diào)用
    void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
    //注銷一個之前(注冊)的回調(diào)函數(shù)
    void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
}

很明顯的可以看見,SharePreference源碼其實是很簡單的。既然這里說了SharePreference類只是一個接口,那么他一定有自己的實現(xiàn)類的,怎么辦呢?我們繼續(xù)往下看。

3-2 SharePreferences實現(xiàn)類SharePreferencesImpl分析

我們從上面SharePreference的使用入口可以分析,具體可以知道SharePreference的實例獲取可以通過兩種方式獲取,一種是Activity的getPreferences方法,一種是Context的getSharedPreferences方法。所以我們?nèi)缦孪葋砜聪逻@兩個方法的源碼。

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

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

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

//ContextImpl類中的靜態(tài)Map聲明,全局的一個sSharedPrefs
private static ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>> sSharedPrefs;

//獲取SharedPreferences實例對象
public SharedPreferences getSharedPreferences(String name, int mode) {
    //SharedPreferences的實現(xiàn)類對象引用聲明
    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();
        //通過包名找到與之關聯(lián)的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關聯(lián)的sp對象
        sp = packagePrefs.get(name);
        if (sp == null) {
            //如果沒找到則先根據(jù)name構(gòu)建一個File的prefsFile對象
            File prefsFile = getSharedPrefsFile(name);
            //依據(jù)上面的File對象創(chuàng)建一個SharedPreferencesImpl對象的實例
            sp = new SharedPreferencesImpl(prefsFile, mode);
            //以key-value方式添加到packagePrefs中
            packagePrefs.put(name, sp);
            返回與name相關的SharedPreferencesImpl對象
            return sp;
        }
    }
    //如果不是第一次,則在3.0之前(默認具備該mode)或者mode為MULTI_PROCESS時調(diào)用reload方法
    if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
            getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
        //重新加載文件數(shù)據(jù)
        sp.startReloadIfChangedUnexpectedly();
    }
    //返回SharedPreferences實例對象sp
    return sp;
}

我們可以發(fā)現(xiàn),上面方法中首先調(diào)運了getSharedPrefsFile來獲取一個File對象,所以我們繼續(xù)先來看下這個方法,具體如下:

    public File getSharedPrefsFile(String name) {
        //依據(jù)我們傳入的文件名字符串創(chuàng)建一個后綴為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文件存儲路徑和文件創(chuàng)建是這個來的。繼續(xù)往下看可以發(fā)現(xiàn)接著調(diào)運了SharedPreferencesImpl的構(gòu)造函數(shù),至于這個構(gòu)造函數(shù)用來干嘛,下面會分析。

好了,到這里我們先回過頭稍微總結(jié)一下目前的源碼分析結(jié)論,具體如下:

前面我們有文章分析了Android中的Context,這里又發(fā)現(xiàn)ContextImpl中有一個靜態(tài)的ArrayMap變量sSharedPrefs。這時候你想到了啥呢?無論有多少個ContextImpl對象實例,系統(tǒng)都共享這一個sSharedPrefs的Map,應用啟動以后首次使用SharePreference時創(chuàng)建,系統(tǒng)結(jié)束時才可能會被垃圾回收器回收,所以如果我們一個App中頻繁的使用不同文件名的SharedPreferences很多時這個Map就會很大,也即會占用移動設備寶貴的內(nèi)存空間,所以說我們應用中應該盡可能少的使用不同文件名的SharedPreferences,取而代之的是合并他們,減小內(nèi)存使用。同時上面最后一段代碼也及具有隱藏含義,其表明了SharedPreferences是可以通過MODE_MULTI_PROCESS來進行夸進程訪問文件數(shù)據(jù)的,其reload就是為了夸進程能更好的刷新訪問數(shù)據(jù)。

好了,還記不記得上面我們分析留的尾巴呢?現(xiàn)在我們就來看看這個尾巴,可以發(fā)現(xiàn)SharedPreferencesImpl類其實就是SharedPreferences接口的實現(xiàn)類,其構(gòu)造函數(shù)如下:

final class SharedPreferencesImpl implements SharedPreferences {
    ......
    //構(gòu)造函數(shù),file是前面分析data目錄下創(chuàng)建的傳入name的xml文件,mode為傳入的訪問方式
    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        //依據(jù)文件名創(chuàng)建一個同名的.bak備份文件,當mFile出現(xiàn)crash的會用mBackupFile來替換恢復數(shù)據(jù)
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        //將文件從flash或者sdcard異步加載到內(nèi)存中
        startLoadFromDisk();
    }
    ......
    //創(chuàng)建同名備份文件
    private static File makeBackupFile(File prefsFile) {
        return new File(prefsFile.getPath() + ".bak");
    }
    ......
    private void startLoadFromDisk() {
        //同步操作mLoaded標志,寫為未加載,這貨是關鍵的關鍵!?。?!
        synchronized (this) {
            mLoaded = false;
        }
        //開啟一個線程異步同步加載disk文件到內(nèi)存
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                synchronized (SharedPreferencesImpl.this) {
                    //新線程中在SharedPreferencesImpl對象鎖中異步load數(shù)據(jù),如果此時數(shù)據(jù)還未load完成,則其他線程調(diào)用SharedPreferences.getXXX方法都會被阻塞,具體原因關注mLoaded標志變量即可?。。。。?                    loadFromDiskLocked();
                }
            }
        }.start();
    }
}

好了,到這里你會發(fā)現(xiàn)整個SharedPreferencesImpl的構(gòu)造函數(shù)很簡單,那我們就繼續(xù)分析真正的異步加載文件到內(nèi)存過程,如下:

    private void loadFromDiskLocked() {
        //如果已經(jīng)異步加載直接return返回
        if (mLoaded) {
            return;
        }
        //如果存在備份文件則直接使用備份文件
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
        ......
        Map map = null;
        StructStat stat = null;
        try {
            //獲取Linux文件stat信息,Linux高級C中經(jīng)常出現(xiàn)的
            stat = Os.stat(mFile.getPath());
            //文件至少是可讀的
            if (mFile.canRead()) {
                BufferedInputStream str = null;
                try {
                    //把文件以BufferedInputStream流讀出來
                    str = new BufferedInputStream(
                            new FileInputStream(mFile), 16*1024);
                    //使用系統(tǒng)提供的XmlUtils工具類將xml流解析轉(zhuǎn)換為map類型數(shù)據(jù)
                    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>();
        }
        //喚醒其他等待線程(其實就是調(diào)運該類的getXXX方法的線程),因為在getXXX時會通過mLoaded標記是否進入wait,所以這里需要notify
        notifyAll();
    }

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

創(chuàng)建相關權(quán)限和mode的xml文件,異步同步鎖加載xml文件并解析xml數(shù)據(jù)為map類型到內(nèi)存中等待使用操作,特別注意,在xml文件異步加載未完成時調(diào)運SharePreference的getXXX及setXXX方法是阻塞等待的。由此也可以知道,一旦拿到SharePreference對象之后的getXXX操作其實都不再是文件讀操作了,也就不存在網(wǎng)上扯蛋的認為多次頻繁使用getXXX方法降低性能的說法了。

分析完了構(gòu)造實例化,我們回憶可以知道使用SharePreference可以通過getXXX方法直接獲取已經(jīng)存在的key-value數(shù)據(jù),下面我們就來看下這個過程,這里我們隨意看一個方法即可,如下:

    public boolean getBoolean(String key, boolean defValue) {
        //可以看見,和上面異步load數(shù)據(jù)使用的是同一個對象鎖
        synchronized (this) {
            //阻塞等待異步加載線程加載完成notify
            awaitLoadedLocked();
            //加載完成后解析的xml數(shù)據(jù)放在mMap對象中,我們從mMap中找出指定key的數(shù)據(jù)
            Boolean v = (Boolean)mMap.get(key);
            //存在返回找到的值,不存在返回設置的defValue
            return v != null ? v : defValue;
        }
    }

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

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

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

3-3 SharePreferences內(nèi)部類Editor實現(xiàn)EditorImpl分析

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

    public Editor edit() {
        //握草!這也和異步load用的一把鎖
        synchronized (this) {
            //阻塞等待,不解釋吧,向上看。。。
            awaitLoadedLocked();
        }
    //異步加載OK以后通過EditorImpl創(chuàng)建Editor實例
        return new EditorImpl();
    }

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

    public final class EditorImpl implements Editor {
        //創(chuàng)建一個mModified的key-value集合,用來在內(nèi)存中暫存數(shù)據(jù)
        private final Map<String, Object> mModified = Maps.newHashMap();
        //一個是否清除preference的flag
        private boolean mClear = false;

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

好了,到此你可以發(fā)現(xiàn)Editor的setXXX及clear操作僅僅只是將相關數(shù)據(jù)暫存到內(nèi)存中或者設置好標記為,也就是說調(diào)運了Editor的putXXX后其實數(shù)據(jù)是沒有存入SharePreference的。那么通過我們一開始的實例可以知道,要想將Editor的數(shù)據(jù)存入SharePreference文件需要調(diào)運Editor的commit或者apply方法來生效。所以我們接下來先來看看Editor類常用的commit方法實現(xiàn)原理,如下:

public boolean commit() {
    //1.先通過commitToMemory方法提交到內(nèi)存
    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.通知數(shù)據(jù)發(fā)生變化了
    notifyListeners(mcr);
    //4.返回寫文件是否成功狀態(tài)
    return mcr.writeToDiskResult;
}

我去,小小一個commit方法做了這么多操作,主要分為四個步驟,我們先來看下第一個步驟,通過commitToMemory方法提交到內(nèi)存返回一個MemoryCommitResult對象。分析commitToMemory方法前先看下MemoryCommitResult這個類,具體如下:

// Return value from EditorImpl#commitToMemory()
//也是內(nèi)部類,只是為了組織數(shù)據(jù)結(jié)構(gòu)而誕生,也就是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();
    }
}

回過頭現(xiàn)在來看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);
        }
        //構(gòu)造數(shù)據(jù)結(jié)構(gòu),把通過SharedPreferencesImpl構(gòu)造函數(shù)里異步加載的文件xml解析結(jié)果mMap賦值給要寫到disk的Map
        mcr.mapToWriteToDisk = mMap;
        //增加一個未完成的寫opt
        mDiskWritesInFlight++;
    //判斷有沒有監(jiān)聽設置
        boolean hasListeners = mListeners.size() > 0;
        if (hasListeners) {
            //創(chuàng)建監(jiān)聽隊列
            mcr.keysModified = new ArrayList<String>();
            mcr.listeners =
                    new HashSet<android.content.SharedPreferences.OnSharedPreferenceChangeListener>(mListeners.keySet());
        }
        //再加一把自己的鎖
        synchronized (this) {
            //如果調(diào)運的是Editor的clear方法,則這里commit時這么處理
            if (mClear) {
                //如果從文件里加載出來的xml不為空
                if (!mMap.isEmpty()) {
                    //設置數(shù)據(jù)結(jié)構(gòu)中數(shù)據(jù)變化標志為true
                    mcr.changesMade = true;
                    //清空內(nèi)存中xml數(shù)據(jù)
                    mMap.clear();
                }
                //處理完畢,標記復位,程序繼續(xù)執(zhí)行,所以如果這次Editor中如果有寫數(shù)據(jù)且還未commit,則執(zhí)行完這次commit之后不會清掉本次寫操作的數(shù)據(jù),只會clear以前xml文件中的所有數(shù)據(jù)
                mClear = false;
            }
        //mModified是調(diào)運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;
                        }
                    }
                    //把變化和新加的數(shù)據(jù)更新到SharePreferenceImpl的mMap中
                    mMap.put(k, v);
                }
            //設置數(shù)據(jù)結(jié)構(gòu)變化標記
                mcr.changesMade = true;
                if (hasListeners) {
                    //設置監(jiān)聽
                    mcr.keysModified.add(k);
                }
            }
            //清空Editor中零時存儲的數(shù)據(jù)
            mModified.clear();
        }
    }
    //返回重新更新過mMap值封裝的數(shù)據(jù)結(jié)構(gòu)
    return mcr;
}   

到此我們Editor的commit方法的第一步已經(jīng)完成,根據(jù)寫操作組織內(nèi)存數(shù)據(jù),返回組織后的數(shù)據(jù)結(jié)構(gòu)。接下來我們繼續(xù)回到commit方法看下第二步—-寫到文件中,其核心是調(diào)運SharedPreferencesImpl類的enqueueDiskWrite方法實現(xiàn)。具體如下:

//按照隊列把內(nèi)存數(shù)據(jù)寫入磁盤,commit時postWriteRunnable為null,apply時不為null
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                              final Runnable postWriteRunnable) {
    //創(chuàng)建一個writeToDiskRunnable的Runnable對象
    final Runnable writeToDiskRunnable = new Runnable() {
        public void run() {
            synchronized (mWritingToDiskLock) {
                //真正的寫文件操作
                writeToFile(mcr);
            }
            synchronized (SharedPreferencesImpl.this) {
                //寫完一個計數(shù)器-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就在線程池中執(zhí)行
    QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

可以發(fā)現(xiàn),commit從內(nèi)存寫文件是在當前調(diào)運線程中直接執(zhí)行的。那我們再來看看這個寫內(nèi)存到磁盤方法中真正的寫方法writeToFile,如下:

    // Note: must hold mWritingToDiskLock
    private void writeToFile(MemoryCommitResult mcr) {
        if (mFile.exists()) {
            if (!mcr.changesMade) {
                //如果文件存在且沒有改變的數(shù)據(jù)則直接返回寫OK
                mcr.setDiskWriteResult(true);
                return;
            }

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

        try {
            //創(chuàng)建mFile文件
            FileOutputStream str = createFileOutputStream(mFile);
            if (str == null) {
                //創(chuàng)建失敗直接返回寫失敗了
                mcr.setDiskWriteResult(false);
                return;
            }
            //把數(shù)據(jù)寫入mFile文件
            XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
            //徹底同步到磁盤文件中
            FileUtils.sync(str);
            str.close();
            //設置文件權(quán)限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()) {
            //上面如果出錯了就刪掉,因為寫之前已經(jīng)備份過數(shù)據(jù)了,下次load時load備份數(shù)據(jù)
            if (!mFile.delete()) {
                Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
            }
        }
        //寫失敗了
        mcr.setDiskWriteResult(false);
    }

回過頭可以發(fā)現(xiàn),上面commit的第二步寫磁盤操作其實是做了類似數(shù)據(jù)庫的事務操作機制的(備份文件)。接著可以繼續(xù)分析commit方法的第三四步,很明顯可以看出,第三步就是回調(diào)設置的監(jiān)聽方法,通知數(shù)據(jù)變化了,第四步就是返回commit寫文件是否成功。

總體到這里你可以發(fā)現(xiàn),一個常用的SharePreferences過程已經(jīng)完全分析完畢。接下來我們就再簡單說說Editor的apply方法原理,先來看下Editor的apply方法,如下:

public void apply() {
    //有了上面commit分析,這個雷同,寫數(shù)據(jù)到內(nèi)存,返回數(shù)據(jù)結(jié)構(gòu)
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
        public void run() {
            try {
                //等待寫文件結(jié)束
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException ignored) {
            }
        }
    };

    QueuedWork.add(awaitCommit);
    //一個收尾的Runnable
    Runnable postWriteRunnable = new Runnable() {
        public void run() {
            awaitCommit.run();
            QueuedWork.remove(awaitCommit);
        }
    };
    //這個上面commit已經(jīng)分析過的,這里postWriteRunnable不為null,所以會在一個新的線程池調(diào)運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類似,只不過他是異步寫的,沒在當前線程執(zhí)行寫文件操作,還有就是他不像commit一樣返回文件是否寫成功狀態(tài)。

3-4 SharePreferences源碼分析總結(jié)

題外趣事: 記得好像是去年有一次我用SharePreferences存儲了幾個boolean值,由于開發(fā)調(diào)試,當時我直接進入系統(tǒng)data目錄下應用的xml存儲文件夾,然后執(zhí)行了刪除操作;接著我沒有重啟應用,直接打斷點調(diào)試,握草!奇跡的發(fā)現(xiàn)SharePreferences調(diào)運get時竟然拿到了不是初始化的值。哈哈,其實就是上面分析的,加載完后是一個靜態(tài)的map,進程沒掛之前一直用的內(nèi)存數(shù)據(jù)。

通過上面的實例及源碼分析可以發(fā)現(xiàn):

  • SharedPreferences在實例化時首先會從sdcard異步讀文件,然后緩存在內(nèi)存中;接下來的讀操作都是內(nèi)存緩存操作而不是文件操作。

  • 在SharedPreferences的Editor中如果用commit()方法提交數(shù)據(jù),其過程是先把數(shù)據(jù)更新到內(nèi)存,然后在當前線程中寫文件操作,提交完成返回提交狀態(tài);如果用的是apply()方法提交數(shù)據(jù),首先也是寫到內(nèi)存,接著在一個新線程中異步寫文件,然后沒有返回值。

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

可以發(fā)現(xiàn),在簡單數(shù)據(jù)行為狀態(tài)存儲中,Android的SharedPreferences是一個安全而且不錯的選擇。

4 SharePreferences進階項目解決方案

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

4-1 SharePreferences存儲復雜對象的解決案例

這個案例完全有些多余(因為看完這個例子你會發(fā)現(xiàn)還不如使用github上大神流行的ACache更爽呢?。且脖容^有意思,所以還是拿出來說說,其實網(wǎng)上實現(xiàn)的也很多。

我們有時候可能會涉及到存儲一個自定義對象到SharedPreferences中,這個怎么實現(xiàn)呢?標準的SharedPreferences的Editor只提供幾個常見類型的put方法呀,其實可以實現(xiàn)的,原理就是Base64轉(zhuǎn)碼為字符串存儲,如下給出我的一個工具類,在項目中可以直接使用:

/**
 * @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;
    }
}

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

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

Android系統(tǒng)有自己的一套安全機制,當應用程序在安裝時系統(tǒng)會分配給他們一個唯一的userid,當該應用程序需要訪問文件等資源的時候必須要匹配userid。默認情況下安卓應用程序創(chuàng)建的各種文件(SharePreferences、數(shù)據(jù)庫等)都是私有的(在/data/data/[APP PACKAGE NAME]/files),其他程序無法訪問。在3.0以后必須在創(chuàng)建文件時顯示的指定了Context.MODE_WORLD_READABLE或者Context.MODE_WORLD_WRITEABLE才能被其他進程(應用)訪問。

這個案例也就類似于Android的ContentProvider,可以跨進程訪問,但是其原理是給權(quán)限然后多進程文件訪問而已。具體我們看如下一個案例,一個用來類比當服務端,一個用來類比當客戶端,如下:

服務端:

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

        mPreferences = getRemotePreferences("com.local.localapp", "local");
    }

    //獲取其他APP的SharedPreferences文件local.xml,特別注意MODE_MULTI_PROCESS的mode
    private SharedPreferences getRemotePreferences(String pkg, String file) {
        try {
            Context remote = createPackageContext(pkg, Context.CONTEXT_IGNORE_SECURITY);
            return remote.getSharedPreferences(file, MODE_WORLD_READABLE | MODE_WORLD_WRITEABLE | MODE_MULTI_PROCESS);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return null;
    }

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

不解釋,需求自己看情況吧,這就是一個經(jīng)典的跨進程訪問SharedPreferences,主要原理就是設置flag然后獲取其他App的Context。

5 SharePreferences總結(jié)

到此整個SharePreferences的使用及原理和進階開發(fā)案例都已經(jīng)全面剖析完畢,相信你對Android的SharePreferences會有一個全新的自己的認識,不再會被網(wǎng)上那些爭議的評論而左右。下一篇文章會繼續(xù)探討Android的SharePreferences拓展控件,即Preference控件家族的使用及源碼分析。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容