Android SharedPreferences 原理簡單分析

先上代碼,在平時(shí)使用時(shí),getSharePreferences的方法源碼如下:


public SharedPreferences getSharedPreferences(String name, int mode) {

                            SharedPreferencesImpl sp;

                            synchronized (ContextImpl.class) {

                                if (sSharedPrefs == null) {

                                    sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();

                                }

                                final String packageName = getPackageName();

                                ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);

                                if (packagePrefs == null) {

                                    packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();

                                    sSharedPrefs.put(packageName, packagePrefs);

                                }

                                // 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.

                                if (mPackageInfo.getApplicationInfo().targetSdkVersion <

                                        Build.VERSION_CODES.KITKAT) {

                                    if (name == null) {

                                        name = "null";

                                    }

                                }

                                sp = packagePrefs.get(name);

                                if (sp == null) {

                                    File prefsFile = getSharedPrefsFile(name);

                                    sp = new SharedPreferencesImpl(prefsFile, mode);

                                    packagePrefs.put(name, 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.

                                sp.startReloadIfChangedUnexpectedly();

                            }

                            return sp;

                        }

可見SDK是先取了緩存(sSharedPrefs靜態(tài)變量),如果緩存未命中,才會開始構(gòu)建新的對象,也就是說多次調(diào)用getSharedPreferences方法,幾乎沒有代價(jià)。而且實(shí)例的構(gòu)造是被包裹在synchronized關(guān)鍵字下,因此該過程是多線程且安全的。

構(gòu)造SharedPreferences

第一次構(gòu)建SharedPreferences對象


// SharedPreferencesImpl.java

SharedPreferencesImpl(File file, int mode) {

    mFile = file;

    mBackupFile = makeBackupFile(file);

    mMode = mode;

    mLoaded = false;

    mMap = null;

    startLoadFromDisk();

}

1.其中mFile代表了磁盤上的配置文件

2.mBackupFile代表了“災(zāi)備”文件,是一個(gè)數(shù)據(jù)寫入失敗時(shí),用以恢復(fù)的文件,該文件路徑為mFile+“.bat”

3.mMap用于在內(nèi)存中存儲我們的配置數(shù)據(jù),也就是getXxx數(shù)據(jù)的來源。

下面重點(diǎn)關(guān)注下 startLoadFromDisk()方法


private void startLoadFromDisk() {

        synchronized (this) {

            mLoaded = false;

        }

        new Thread("SharedPreferencesImpl-load") {

            public void run() {

                loadFromDisk();

            }

        }.start();

    }

這里他開啟了一個(gè)從Disk讀取的線程,即:


// SharedPreferencesImpl.java

private void loadFromDisk() {

    synchronized (SharedPreferencesImpl.this) {

        if (mLoaded) {

            return;

        }

        if (mBackupFile.exists()) {

            mFile.delete();

            mBackupFile.renameTo(mFile);

        }

    }

    ..略去無關(guān)代碼 ..

    str = new BufferedInputStream(

            new FileInputStream(mFile), 16*1024);

    map = XmlUtils.readMapXml(str);

    synchronized (SharedPreferencesImpl.this) {

        mLoaded = true;

        if (map != null) {

            mMap = map;

            mStatTimestamp = stat.st_mtime;

            mStatSize = stat.st_size;

        } else {

            mMap = new HashMap<>();;

        }

        notifyAll();

    }

}

loadFormDisk()在整個(gè)過程中非常關(guān)鍵,這其中它做了以下幾件事:

1.如果有災(zāi)備文件,則直接使用災(zāi)備文件回滾。

2.把配置從磁盤文件中讀取到內(nèi)存被保存到mMap字段中, 即 mMap=map;

3.標(biāo)記讀取完成,這個(gè)字段在后面的awaitLoadLocked會用到,記錄讀取文件的時(shí)間,后面MODE_MULTI_PROCESS 中會用到

4.發(fā)一個(gè)notifyAll通知已經(jīng)讀取完畢,激活其他所有等待加載中的線程。

在這里借用一張圖來演示其整個(gè)過程:

image

getX原理分析


   public float getFloat(String key, float defValue) {

        synchronized (this) {

            awaitLoadedLocked();

            Float v = (Float)mMap.get(key);

            return v != null ? v : defValue;

        }

    }

1.synchronized保證了線程安全

2.get操作一定是從mMap中讀取,即從內(nèi)存中讀取,無過多性能損耗。

3awaitLoadedLocked()保證了讀取操作一定是在loadFormDisk()執(zhí)行完成之后,同步等待。因此在第一次調(diào)用get操作時(shí),可能會發(fā)生阻塞,特別注意:這也是為什么sp會被定義為輕量級存儲系統(tǒng)的重要原因

putX原理分析

相對get操作,put操作會復(fù)雜一些。

創(chuàng)建editor


// SharedPreferencesImpl.java

    public Editor edit() {

        // TODO: remove the need to call awaitLoadedLocked() when

        // requesting an editor.  will require some work on the

        // Editor, but then we should be able to do:

        //

        //      context.getSharedPreferences(..).edit().putString(..).apply()

        //

        // .. all without blocking.

        synchronized (this) {

            awaitLoadedLocked();

        }

        return new EditorImpl();

    }

這里的EditorImpl()無構(gòu)造函數(shù),僅僅去初始化兩個(gè)成員變量


// SharedPreferencesImpl.java

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) { ... }

    public boolean commit() { ... }

    ..

}

1.mModified是我們每次putXxx后所改變的配置

2.mClear標(biāo)識要清空配置項(xiàng)

putString


// SharedPreferencesImpl.java

public void apply() {

    final MemoryCommitResult mcr = commitToMemory();

    .. 略無關(guān) ..

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

}

這里是吧我們的配置項(xiàng)放到mModified屬性里保存。等到apply或commit的時(shí)候回寫到內(nèi)存和磁盤。

關(guān)于apply和commit我們需要分別來看,即可它們之間的區(qū)別。


// SharedPreferencesImpl.java

public void apply() {

    final MemoryCommitResult mcr = commitToMemory();

    ... 略無關(guān) ...

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

}

apply的核心在于兩點(diǎn):

1.commitToMemory完成了內(nèi)存的同步回寫。
2.enqueueDiskWrite完成了磁盤異步回寫具體如下:

// SharedPreferencesImpl.java
private MemoryCommitResult commitToMemory() {
    MemoryCommitResult mcr = new MemoryCommitResult();
    synchronized (SharedPreferencesImpl.this) {

        ... 略去無關(guān) ...

        mcr.mapToWriteToDisk = mMap;
        mDiskWritesInFlight++;

        synchronized (this) {
            for (Map.Entry&lt;String, Object&gt; e : mModified.entrySet()) {
                String k = e.getKey();
                Object v = e.getValue();
                // &quot;this&quot; is the magic value for a removal mutation. In addition,
                // setting a value to &quot;null&quot; for a given key is specified to be
                // equivalent to calling remove on that key.
                if (v == this || v == null) {
                    mMap.remove(k);
                } else {
                    mMap.put(k, v);
                }
            }

            mModified.clear();
        }
    }
    return mcr;
}

兩個(gè)關(guān)鍵點(diǎn)

1.把Editor.mModified中的配置項(xiàng)回寫到SharedPreferences.mMap中,完成了內(nèi)存的同步回寫。
2.把shareadPreference.mMap保存在mcr.mapTOWriteToDisk中,而后者就是即將要回寫到磁盤中的數(shù)據(jù)源。

// SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                final Runnable postWriteRunnable) {
    final Runnable writeToDiskRunnable = new Runnable() {
            public void run() {
                synchronized (mWritingToDiskLock) {
                    writeToFile(mcr);
                }

                ...
            }
        };

    ...

    QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

使用了singgleThreadExecutor單一線程池去依次執(zhí)行寫入磁盤的runnable序列。

在這之后是真正執(zhí)行把數(shù)據(jù)寫入磁盤的方法:

private void writeToFile(MemoryCommitResult mcr) {
    // Rename the current file so it may be used as a backup during the next read
    if (mFile.exists()) {
        if (!mBackupFile.exists()) {
            if (!mFile.renameTo(mBackupFile)) {
                return;
            }
        } else {
            mFile.delete();
        }
    }

    // Attempt to write the file, delete the backup and return true as atomically as
    // possible.  If any exception occurs, delete the new file; next time we will restore
    // from the backup.
    try {
        FileOutputStream str = createFileOutputStream(mFile);
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
        ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
        try {
            final StructStat stat = Os.stat(mFile.getPath());
                mStatTimestamp = stat.st_mtime;
                mStatSize = stat.st_size;
        }
        // Writing was successful, delete the backup file if there is one.
        mBackupFile.delete();
        return;
    }

    // Clean up an unsuccessfully written file
    mFile.delete();
}

整段代碼下來,主要分為三個(gè)過程:

1.先把已存的老配置文件復(fù)制重命名,即加上.bak作為災(zāi)備文件,然后刪除老的配置文件刪除。相當(dāng)于做的備份處理
2.想mFile一次性寫入所有的配置項(xiàng),即mcr.mMapToWriteToDisk(即上文提到被mMap賦值后即將寫入數(shù)據(jù))一次性寫入到磁盤之中,如果寫入成功則刪除.bak的災(zāi)備文件,同時(shí)記錄這次同步的時(shí)間。
3.如果上述過程中2失敗,則會刪除這個(gè)半成品配置文件。

apply總結(jié):

1.通過commitToMemory將配置項(xiàng)同步會寫到內(nèi)存SharePreferences .mMap中,此時(shí),任何的getXxx都可以獲得最新的數(shù)據(jù)了。
2.通過enqueDiskWrite調(diào)用writeFile將所有配置項(xiàng)一次性異步會寫到磁盤,這是一個(gè)單線程的線程池

具體流程可總結(jié)為下圖:

SharedPreferencesApply.png

Commit

commit比較簡單,直接看代碼和時(shí)序圖即可,大致和apply相同、

public boolean commit() {
    MemoryCommitResult mcr = commitToMemory();
    SharedPreferencesImpl.this.enqueueDiskWrite(
        mcr, null /* sync write on this thread okay */);
    try {
        mcr.writtenToDiskLatch.await();
    } catch (InterruptedException e) {
        return false;
    }
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}
SharedPreferencesCommit.png

可以看得出,commit會等待異步任務(wù)的返回,說明會阻塞當(dāng)前調(diào)用線程,也因此說commit是同步寫入,apply是異步寫入。

在了解完整個(gè)SharedPreferences的代碼原理之后,我們不難發(fā)現(xiàn)它的一些特點(diǎn)和基本概念,所以下面我們可以試著總結(jié)出一些平時(shí)在使用SharedPreferences時(shí)一些需要注意的事項(xiàng),和所需要規(guī)避的事情:

勿存儲過大value

永遠(yuǎn)記住,SharedPreferences是一個(gè)輕量級的存儲系統(tǒng),不要存過多且復(fù)雜的數(shù)據(jù),這會帶來以下的問題
第一次從sp中獲取值的時(shí)候,有可能阻塞主線程,使界面卡頓、掉幀。
這些key和value會永遠(yuǎn)存在于內(nèi)存之中,占用大量內(nèi)存。

勿存儲復(fù)雜數(shù)據(jù)

SharedPreferences通過xml存儲解析,JSON或者HTML格式存放在sp里面的時(shí)候,需要轉(zhuǎn)義,這樣會帶來很多&這種特殊符號,sp在解析碰到這個(gè)特殊符號的時(shí)候會進(jìn)行特殊的處理,引發(fā)額外的字符串拼接以及函數(shù)調(diào)用開銷。如果數(shù)據(jù)量大且復(fù)雜,嚴(yán)重時(shí)可能導(dǎo)頻繁GC。

不要亂edit和apply,盡量批量修改一次提交

edit會創(chuàng)建editor對象,每進(jìn)行一次apply就會創(chuàng)建線程,進(jìn)行內(nèi)存和磁盤的同步,千萬寫類似下面的代碼
SharedPreferences sp = getSharedPreferences("test", MODE_PRIVATE);
sp.edit().putString("test1", "sss").apply();
sp.edit().putString("test2", "sss").apply();
sp.edit().putString("test3", "sss").apply();
sp.edit().putString("test4", "sss").apply();

建議apply,少用commit

commit同步寫內(nèi)存,同步寫磁盤。有是否成功的返回值
apply同步寫內(nèi)存,異步寫磁盤。無返回值

registerOnSharedPreferenceChangeListener弱引用問題

見本文初

apply和commit對registerOnSharedPreferenceChangeListener的影響

對于 apply, listener 回調(diào)時(shí)內(nèi)存已經(jīng)完成同步, 但是異步磁盤任務(wù)不保證是否完成
對于 commit, listener 回調(diào)時(shí)內(nèi)存和磁盤都已經(jīng)同步完畢

不要有任何用SP進(jìn)行多進(jìn)程存儲的幻想

這個(gè)話題不需要過多討論,只記住一點(diǎn),多進(jìn)程別用SP,Android沒有對SP在多進(jìn)程上的表現(xiàn)做任何約束和保證。附上Google官方注釋:

@deprecated MODE_MULTI_PROCESS does not work reliably in
some versions of Android, and furthermore does not provide any mechanism for reconciling concurrent modifications across processes. Applications should not attempt to use it. Instead, they should use an explicit cross-process data management approach such as {@link android.content.ContentProvider ContentProvider}.
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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