SharedPreferences

Start

前言

轉(zhuǎn)載
我們知道 SharedPreferences 會從文件讀取 xml 文件, 并將其以 getXxx/putXxx 的形式提供讀寫服務. 其中涉及到如下幾個問題:

  1. 如何從磁盤讀取配置到內(nèi)存
  2. getXxx 如何從內(nèi)存中獲取配置
  3. 最終配置如何從內(nèi)存回寫到磁盤
  4. 多線程/多進程是否會有問題
  5. 最佳實踐

1. 結論

  • SharedPreferences 是線程安全的峡钓,內(nèi)部由大量 synchronized 關鍵字保障。
  • SharedPreferences 不是進程安全的盐固。
  • 第一次 getSharedPreferences 會讀取磁盤文件,后續(xù)的 getSharedPreferences 會從內(nèi)存緩存中獲取实幕。 如果第一次調(diào)用 getSharedPreferences 時還沒從磁盤加載完畢就調(diào)用 getXxx/putXxx 无埃, 則 getXxx/putXxx 操作會卡主,直到數(shù)據(jù)從磁盤加載完畢后返回山宾。
  • 所有的 getXxx 都是從內(nèi)存中取的數(shù)據(jù)
  • apply 是同步回寫內(nèi)存俏让,然后把異步回寫磁盤的任務放到一個單線程的隊列中等待調(diào)度楞遏。commit 和前者一樣,只不過要等待異步磁盤任務結束后才返回首昔。
  • MODE_MULTI_PROCESS 是在每次 getSharedPreferences 時檢查磁盤上配置文件上次修改時間和文件大小寡喝, 一旦所有修改則會重新從磁盤加載文件。 所以并不能保證多進程數(shù)據(jù)的實時同步勒奇。
  • 從 Android N 開始预鬓,不再支持 MODE_WORLD_READABLE & MODE_WORLD_WRITEABLE。一旦指定赊颠,會拋異常格二。

2. 最佳實踐

  • 不要多進程使用,很小幾率會造成數(shù)據(jù)全部丟失(萬分之一)竣蹦, 現(xiàn)象是配置文件被刪除顶猜。
  • 不要依賴 MODE_MULTI_PROCESS,這個標記就像 MODE_WORLD_READABLE/MODE_WORLD_WRITEABLE 未來會被廢棄痘括。
  • 每次 apply / commit 都會把全部的數(shù)據(jù)一次性寫入磁盤密任,所以單個的配置文件不應該過大涂炎, 影響整體性能碳默。

3. 源碼分析

3.1 SharedPreferences 對象的獲取

一般來說有如下方式:

  1. PreferenceManager.getDefaultSharedPreferences
  2. ContextImpl.getSharedPreferences

我們以上述 [1] 為例來看看源碼:

// PreferenceManager.java
public static SharedPreferences getDefaultSharedPreferences(Context context) {
    return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
            getDefaultSharedPreferencesMode());
}

可以看到最終也是調(diào)用到了 ContextImpl.getSharedPreferences, 源碼:

// ContextImpl.java
@Override
    public SharedPreferences getSharedPreferences(File file, int mode) {
        SharedPreferencesImpl sp;
        synchronized (ContextImpl.class) {
            final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
            sp = cache.get(file);
            if (sp == null) {
                checkMode(mode);
                if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
                    if (isCredentialProtectedStorage()
                            && !getSystemService(UserManager.class)
                                    .isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
                        throw new IllegalStateException("SharedPreferences in credential encrypted "
                                + "storage are not available until after user is unlocked");
                    }
                }
                sp = new SharedPreferencesImpl(file, mode);
                cache.put(file, 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 是先取了緩存猛遍,如果緩存未命中,才構造對象驰后。也就是說,多次 getSharedPreferences 幾乎是沒有代價的矗愧。 同時灶芝, 實例的構造被 synchronized 關鍵字包裹郑原,因此構造過程是多線程安全的。

3.2 SharedPreferences 的構造

第一次構建對象時:

// SharedPreferencesImpl.java
SharedPreferencesImpl(File file, int mode) {
    mFile = file;
    mBackupFile = makeBackupFile(file);
    mMode = mode;
    mLoaded = false;
    mMap = null;
    startLoadFromDisk();
}

有這么幾個關鍵信息:

  1. mFile 代表我們磁盤上的配置文件夜涕。
  2. mBackupFile 是一個災備文件犯犁,用戶寫入失敗時進行恢復,后面會再說女器。其路徑是 mFile 加后綴 ‘.bak’酸役。
  3. mMap 用于在內(nèi)存中緩存我們的配置數(shù)據(jù), 也就是 getXxx 數(shù)據(jù)的來源。

還涉及到一個 startLoadFromDisk驾胆, 我們來看看:

// SharedPreferencesImpl.java
    private void startLoadFromDisk() {
        synchronized (mLock) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }

開啟了一個線程從文件讀取涣澡, 其源碼如下:

// SharedPreferencesImpl.java
private void loadFromDisk() {
    synchronized (SharedPreferencesImpl.this) {
        if (mLoaded) {
            return;
        }
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
    }

    ... 略去無關代碼 ...

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

loadFromDisk 這個函數(shù)很關鍵。它就是實際從磁盤讀取配置文件的函數(shù)丧诺。 可見入桂, 它做了如下幾件事:

  1. 如果有 ‘災備’ 文件,則直接使用災備文件回滾驳阎。
  2. 把配置從磁盤讀取到內(nèi)存的并保存在 mMap 字段中(看代碼最后 mMap = map)抗愁。
  3. 標記讀取完成, 這個字段后面 awaitLoadedLocked 會用到呵晚。 記錄讀取文件的時間蜘腌,后面 MODE_MULTI_PROCESS 中會用到。
  4. 發(fā)一個 notifyAll 通知已經(jīng)讀取完畢饵隙, 激活所有等待加載的其他線程撮珠。

總結一下:


從磁盤讀取配置

3.3 getXxx 的流程

// SharedPreferencesImpl.java
public String getString(String key, @Nullable String defValue) {
    synchronized (this) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

可見, 所有的 get 操作都是線程安全的癞季。并且 get 僅僅是從內(nèi)存中(mMap) 獲取數(shù)據(jù)劫瞳, 所以無性能問題。

考慮到配置文件的加載是在單獨的線程中異步進行的(參考 ‘SharedPreferences 的構造’)绷柒,所以這里的 awaitLoadedLocked 是在等待配置文件加載完畢志于。 也就是說如果我們第一次構造 SharedPreferences 后就立刻調(diào)用 getXxx 方法, 很有可能讀取配置文件的線程還未完成废睦, 所以這里要等待該線程做完相應的加載工作伺绽。

來看看 awaitLoadedLocked 的源碼:

// SharedPreferencesImpl.java 
  private void awaitLoadedLocked() {
        if (!mLoaded) {
            // Raise an explicit StrictMode onReadFromDisk for this
            // thread, since the real read will be in a different
            // thread and otherwise ignored by StrictMode.
            BlockGuard.getThreadPolicy().onReadFromDisk();
        }
        while (!mLoaded) {
            try {
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
        if (mThrowable != null) {
            throw new IllegalStateException(mThrowable);
        }
    }

很明顯,如果加載還未完成(mLoaded == false)嗜湃,getXxx 會卡在 awaitLoadedLocked奈应,一旦加載配置文件的線程工作完畢, 則這個加載線程會通過 notifyAll 會通知所有在 awaitLoadedLocked 中等待的線程购披,getXxx 就能夠返回了杖挣。不過大部分情況下,mLoaded == true刚陡。這樣的話 awaitLoadedLocked 會直接返回惩妇。

3.4 putXxx 的流程

setget 稍微麻煩一點兒株汉,因為涉及到 EditorMemoryCommitResult 對象。

先來看看 edit() 方法的實現(xiàn):

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

Editor

Editor 沒有構造函數(shù)歌殃,只有兩個屬性被初始化:

// SharedPreferencesImpl.java
public final class EditorImpl implements Editor {
    private final Object mEditorLock = new Object();

    @GuardedBy("mEditorLock")
    private final Map<String, Object> mModified = new HashMap<>();

    @GuardedBy("mEditorLock")
    private boolean mClear = false;

    ... 略去方法定義 ...
    public Editor putString(String key, @Nullable String value) { ... }
    public boolean commit() { ... }
    ...
}

mModified 是我們每次 putXxx 后所改變的配置項乔妈。
mClear 標識要清空配置項, 但是只清了 SharedPreferences.mMap 氓皱。

edit() 會保障配置已從磁盤讀取完畢路召,然后僅僅創(chuàng)建了一個對象。接下來看看 putXxx 的真身:

// SharedPreferencesImpl.java
public Editor putString(String key, @Nullable String value) {
    synchronized (this) {
        mModified.put(key, value);
        return this;
    }
}

很簡單波材,僅僅是把我們設置的配置項放到了 mModified 屬性里保存股淡。等到 apply 或者 commit 的時候回寫到內(nèi)存和磁盤。咱們分別來看看各聘。

apply

apply 是各種 ‘最佳實踐’ 推薦的方式揣非,那么它到底是怎么異步工作的呢?我們來看個究竟:

// SharedPreferencesImpl.java
public void apply() {
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
            public void run() {
                try {
                    mcr.writtenToDiskLatch.await();
                } catch (InterruptedException ignored) {
                }
            }
        };

    QueuedWork.add(awaitCommit);

    Runnable postWriteRunnable = new Runnable() {
            public void run() {
                awaitCommit.run();
                QueuedWork.remove(awaitCommit);
            }
        };

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);

    // Okay to notify the listeners before it&#039;s hit disk
    // because the listeners should always get the same
    // SharedPreferences instance back, which has the
    // changes reflected in memory.
    notifyListeners(mcr);
}

可以看出大致的脈絡:

  1. commitToMemory 應該是把修改的配置項回寫到內(nèi)存躲因。
  2. QueuedWork.add(awaitCommit) 貌似沒什么卵用早敬。
  3. SharedPreferencesImpl.this.enqueueDiskWrite 把配置項加入到一個異步隊列中,等待調(diào)度大脉。

我們來看看 commitToMemory 的實現(xiàn)(略去大量無關代碼):

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

        ... 略去無關 ...

        mapToWriteToDisk = mMap;
                mDiskWritesInFlight++;

                boolean hasListeners = mListeners.size() > 0;
                if (hasListeners) {
                    keysModified = new ArrayList<String>();
                    listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
                }

                synchronized (mEditorLock) {
                    boolean changesMade = false;

                    if (mClear) {
                        if (!mapToWriteToDisk.isEmpty()) {
                            changesMade = true;
                            mapToWriteToDisk.clear();
                        }
                        mClear = false;
                    }

                    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.
                        if (v == this || v == null) {
                            if (!mapToWriteToDisk.containsKey(k)) {
                                continue;
                            }
                            mapToWriteToDisk.remove(k);
                        } else {
                            if (mapToWriteToDisk.containsKey(k)) {
                                Object existingValue = mapToWriteToDisk.get(k);
                                if (existingValue != null && existingValue.equals(v)) {
                                    continue;
                                }
                            }
                            mapToWriteToDisk.put(k, v);
                        }

                        changesMade = true;
                        if (hasListeners) {
                            keysModified.add(k);
                        }
                    }

                    mModified.clear();

                    if (changesMade) {
                        mCurrentMemoryStateGeneration++;
                    }

                    memoryStateGeneration = mCurrentMemoryStateGeneration;
                }
            }
            return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
                    mapToWriteToDisk);
        }
}

總結來說就兩件事:

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

我們再來回頭看看 apply 方法:

// SharedPreferencesImpl.java
public void apply() {
    final MemoryCommitResult mcr = commitToMemory();

    ... 略無關 ...

    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
}
  • commitToMemory 完成了內(nèi)存的同步回寫
  • enqueueDiskWrite 完成了硬盤的異步回寫, 我們接下來具體看看 enqueueDiskWrite
// 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);
}

QueuedWork.singleThreadExecutor 實際上就是 ‘一個線程的線程池’, 如下:

// QueuedWork.java
public static ExecutorService singleThreadExecutor() {
    synchronized (QueuedWork.class) {
        if (sSingleThreadExecutor == null) {
            // TODO: can we give this single thread a thread name?
            sSingleThreadExecutor = Executors.newSingleThreadExecutor();
        }
        return sSingleThreadExecutor;
    }
}

回到 enqueueDiskWrite 中秤标,這里還有一個重要的函數(shù)叫做 writeToFile:

writeToFile

// SharedPreferencesImpl.java
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();
}

代碼大致分為三個過程:

  1. 先把已存在的老的配置文件重命名(加 ‘.bak’ 后綴)绝淡, 然后刪除老的配置文件。這相當于做了災備苍姜。
  2. mFile 中一次性寫入所有配置項牢酵。即 mcr.mapToWriteToDisk(這就是 commitToMemory 所說的保存了所有配置項的字段) 一次性寫入到磁盤。如果寫入成功則刪除災備文件衙猪,同時記錄了這次同步的時間馍乙。
  3. 如果上述過程 [2] 失敗,則刪除這個半成品的配置文件垫释。

好了, 我們來總結一下 apply:

  1. 通過 commitToMemory 將修改的配置項同步回寫到內(nèi)存 SharedPreferences.mMap 中丝格。此時,任何的 getXxx 都可以獲取到最新數(shù)據(jù)了棵譬。
  2. 通過 enqueueDiskWrite 調(diào)用 writeToFile 將所有配置項一次性異步回寫到磁盤显蝌, 這是一個單線程的線程池。

時序圖:

apply 時序圖

commit

看過了 apply 再看 commit 就非常容易了订咸。

// SharedPreferencesImpl.java
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;
}

時序圖:

commit 時序圖

只需關注最后一條 ‘等待異步任務返回’ 的線曼尊,對比 apply 的時序圖, 一眼就看出差別扭屁。

registerOnSharedPreferenceChangeListener

最后需要提一下的就是 listener:

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

申明:開始的圖片來源網(wǎng)絡艾船,侵刪

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市高每,隨后出現(xiàn)的幾起案子屿岂,更是在濱河造成了極大的恐慌,老刑警劉巖鲸匿,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件爷怀,死亡現(xiàn)場離奇詭異,居然都是意外死亡带欢,警方通過查閱死者的電腦和手機运授,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來乔煞,“玉大人吁朦,你說我怎么就攤上這事《杉郑” “怎么了逗宜?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長空骚。 經(jīng)常有香客問我纺讲,道長,這世上最難降的妖魔是什么囤屹? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任熬甚,我火速辦了婚禮,結果婚禮上牺丙,老公的妹妹穿的比我還像新娘则涯。我一直安慰自己,他們只是感情好冲簿,可當我...
    茶點故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布粟判。 她就那樣靜靜地躺著,像睡著了一般峦剔。 火紅的嫁衣襯著肌膚如雪档礁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天吝沫,我揣著相機與錄音呻澜,去河邊找鬼递礼。 笑死,一個胖子當著我的面吹牛羹幸,可吹牛的內(nèi)容都是我干的脊髓。 我是一名探鬼主播,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼栅受,長吁一口氣:“原來是場噩夢啊……” “哼将硝!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起屏镊,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤依疼,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后而芥,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體律罢,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年棍丐,在試婚紗的時候發(fā)現(xiàn)自己被綠了误辑。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡歌逢,死狀恐怖稀余,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情趋翻,我是刑警寧澤睛琳,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站踏烙,受9級特大地震影響师骗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜讨惩,卻給世界環(huán)境...
    茶點故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一辟癌、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧荐捻,春花似錦黍少、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至魂角,卻和暖如春昵济,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工访忿, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留瞧栗,地道東北人。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓海铆,卻偏偏與公主長得像迹恐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子卧斟,可洞房花燭夜當晚...
    茶點故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容