Android 之不要濫用 SharedPreferences(下)

閃存
Android 存儲優(yōu)化系列專題
  • SharedPreferences 系列

Android 之不要濫用 SharedPreferences
Android 之不要濫用 SharedPreferences(2)— 數(shù)據(jù)丟失

  • ContentProvider 系列(待更)

《Android 存儲選項之 ContentProvider 啟動過程源碼分析》
《Android 存儲選項之 ContentProvider 深入分析》

  • 對象序列化系列

Android 對象序列化之你不知道的 Serializable
Android 對象序列化之 Parcelable 深入分析
Android 對象序列化之追求完美的 Serial

  • 數(shù)據(jù)序列化系列(待更)

《Android 數(shù)據(jù)序列化之 JSON》
《Android 數(shù)據(jù)序列化之 Protocol Buffer 使用》
《Android 數(shù)據(jù)序列化之 Protocol Buffer 源碼分析》

  • SQLite 存儲系列

Android 存儲選項之 SQLiteDatabase 創(chuàng)建過程源碼分析
Android 存儲選項之 SQLiteDatabase 源碼分析
數(shù)據(jù)庫連接池 SQLiteConnectionPool 源碼分析
SQLiteDatabase 啟用事務(wù)源碼分析
SQLite 數(shù)據(jù)庫 WAL 模式工作原理簡介
SQLite 數(shù)據(jù)庫鎖機(jī)制與事務(wù)簡介
SQLite 數(shù)據(jù)庫優(yōu)化那些事兒


在上篇《Android 之不要濫用 SharedPreferences》一文,詳細(xì)為大家分析了關(guān)于 SharedPreferences 存儲機(jī)制以及對它的不當(dāng)使用,可能引發(fā)的“嚴(yán)重后果”。本文也是建立在該基礎(chǔ)之上進(jìn)一步對 SharedPreferences 可能導(dǎo)致數(shù)據(jù)丟失場景進(jìn)行分析。如果你對 SharedPreferences 機(jī)制還不熟悉的話,可以先去參考下。

先來簡單回顧下:SharedPreferences 是 Android 中比較常用的存儲方法,它可以用來存儲一些比較小的鍵值對集合。雖然 SharedPreferences 使用非常簡便,但也是我們詬病比較多的存儲方法。它的性能問題比較多,我可以輕松說出它的“幾宗罪”。

  1. 跨進(jìn)程不安全。由于沒有使用跨進(jìn)程的鎖,就算使用 MODE_MULTI_PROCESS,SharedPreferences 在跨進(jìn)程頻繁讀寫有可能導(dǎo)致數(shù)據(jù)全部丟失。根據(jù)線上統(tǒng)計,SharedPreferences 大約會有萬分之一的損壞率。

  2. 加載緩慢。SharedPreferences 文件的加載使用了異步線程,而且加載線程并沒有設(shè)置優(yōu)先級,如果這個時候讀取數(shù)據(jù)就需要等待文件加載線程的結(jié)束。這就導(dǎo)致主線程等待低優(yōu)先線程鎖的問題,比如一個 100KB 的 SP 文件讀取等待時間大約需要 50 ~ 100ms,并且建議大家提前用預(yù)加載啟動過程用到的 SP 文件。

  3. 全量寫入。無論是 commit() 還是 apply(),即使我們只改動其中一個條目,都會把整個內(nèi)容全部寫到文件。而且即使我們多次寫同一個文件,SP 也沒有將多次修改合并為一次,這也是性能差的重要原因之一。

  4. 卡頓。由于提供了異步落盤的 apply 機(jī)制,在崩潰或者其它一些異常情況可能會導(dǎo)致數(shù)據(jù)丟失。所以當(dāng)應(yīng)用收到系統(tǒng)廣播,或者被調(diào)用 onPause 等一些時機(jī),系統(tǒng)會強(qiáng)制把所有的 SharedPreferences 對象的數(shù)據(jù)落地到磁盤。如果沒有落地完成,這時候主線程會被一直阻塞。這樣非常容易造成卡頓,甚至是ANR,從線上數(shù)據(jù)來看 SP 卡頓占比一般會超過 5%。

坦白來講,系統(tǒng)提供的 SharedPreferences 的應(yīng)用場景是用來存儲一些非常簡單、輕量的數(shù)據(jù)。我們不要使用它存儲過于復(fù)雜的數(shù)據(jù),例如 HTML、JSON 等。而且 SharedPreferences 的文件存儲性能與文件大小有關(guān),每個 SP 文件不能過大,我們不要將毫無關(guān)聯(lián)的配置項保存在同一個文件中,同時考慮將頻繁修改的條目單獨隔離出來。

數(shù)據(jù)丟失分析

SharedPrefenerces 提供了線程安全操作(內(nèi)部有大量Synchronized方法),但是并不能保證跨進(jìn)程數(shù)據(jù)的安全,也就是在跨進(jìn)程訪問時可能會導(dǎo)致文件損壞(但并不局限于多進(jìn)程場景)。

1、疑問:文件為什么會損壞?

為什么會文件損壞?在回答該問題之前先要明確一下什么是文件損壞?一個文件的格式或者內(nèi)容,如果沒有按照應(yīng)用程序?qū)懭氲慕Y(jié)果都屬于文件損壞。它不只是文件格式錯誤,文件內(nèi)容丟失可能才是最常出現(xiàn)的,SharedPreferences 跨進(jìn)程讀寫就非常容易出現(xiàn)數(shù)據(jù)丟失的情況。

我們可以從應(yīng)用程序、文件系統(tǒng)和磁盤三個角度來審視這個問題。

  • 應(yīng)用程序。大部分的 I/O 方法都不是原子操作的,文件的跨進(jìn)程或者多線程寫入、使用一個已經(jīng)關(guān)閉的文件描述符 fd 來操作文件,它們都有可能導(dǎo)致數(shù)據(jù)被覆蓋或者刪除。事實上,大部分的文件損壞都是因為應(yīng)用程序代碼設(shè)計考慮不當(dāng)導(dǎo)致的,并不是文件系統(tǒng)或者磁盤的問題。

  • 文件系統(tǒng)。雖說內(nèi)核崩潰或者系統(tǒng)突然斷電都有可能導(dǎo)致文件系統(tǒng)損壞,不過文件系統(tǒng)也做了很多的保護(hù)措施。例如 system 分區(qū)保證只讀不可寫,增加異常檢查和恢復(fù)機(jī)制等。

在文件系統(tǒng)這一層,更多是因為斷電而導(dǎo)致的寫入丟失。為了提升 I/O 性能,文件系統(tǒng)把數(shù)據(jù)寫入到 Page Cache 中,然后等待合適的時機(jī)才會真正的寫入磁盤。當(dāng)然我們也可以通過 fsync、msync 這些接口強(qiáng)制寫入磁盤。(SharedPreferences 在落盤時就使用了sync 機(jī)制)

  • 磁盤。手機(jī)上使用的閃存是電子式的存儲設(shè)備,所以資料傳輸過程可能會發(fā)生電子遺失等現(xiàn)象導(dǎo)致數(shù)據(jù)錯誤。不過閃存也會使用 ECC、多級編碼等方式增加數(shù)據(jù)的可靠性,一般來說出現(xiàn)這種情況的可能性比較小。

接下來還是結(jié)合源碼的角度與大家一起重點探討下 SharedPreferences 的落盤機(jī)制:

2、SharedPreferences 的備份文件

再回顧下我們通過 Context.getSharedPreferences(name),得到的實際類型是:SharedPreferencesImpl。有關(guān) SharedPreferencesImpl 的機(jī)制在上篇文章中已經(jīng)詳細(xì)分析過。SharedPreferencesImpl 的構(gòu)造方法,如下圖:

SharedPreferenceImpl 的構(gòu)造方法

注意源碼中 mBackupFile 變量,本文也是重點圍繞該變量進(jìn)行分析。

創(chuàng)建 SharedPreferences 備份文件

從這可以看出,mBackupFile 是原始文件的備份文件,如:.../config.xml.bak(config 為 SharedPreferences 的文件名)。

3、mBackupFile 備份文件的作用

無論我們使用 SharedPreferences 的 commit() 或 apply() 提交數(shù)據(jù),都會調(diào)用到 writeToFile 方法:


提交流程

這里只給大家簡單貼下調(diào)用棧,commit 提交方法如下:

commit 提交

enqueueDiskWrite 方法如下:

enqueueDiskWrite 方法

可以看到 wirteToFile 方法的調(diào)用時機(jī),這也是我們要重點追蹤的方法。wirteToFile 方法的作用是將我們前面一系列的 putXxx 或 remove 后的數(shù)據(jù)落盤到存儲設(shè)備(在移動設(shè)備一般指的是 Flash 閃存)。

4、寫入文件分析

由于 writeToFile 方法內(nèi)容較多,我們分上下兩個部分分析:

writeToFile 方法,執(zhí)行數(shù)據(jù)落盤

省去部分日志代碼,代碼中也標(biāo)注了詳細(xì)的注釋:

首先如果源文件存在(SharedPreferences 文件,這里相對它的備份文件而言),判斷如果要寫入的數(shù)據(jù)是否真正發(fā)生變化,如果未發(fā)生變化則直接 return,這算是一層優(yōu)化,避免無謂的 I/O 操作。

注意判斷數(shù)據(jù)是否真正發(fā)生變化是在 EditorImpl 的 commmitToMemory() 方法中,在上篇文章中也有分析到:當(dāng)前一系列操作數(shù)據(jù)發(fā)生在 EditorImpl 的 mModified(Map)變量中,該方法會比較 mModified 與 SharedPreferencesImpl 中 mMap 后修正最后一次 mMap 中數(shù)據(jù),如果數(shù)據(jù)發(fā)生改變,如下圖:

當(dāng)前數(shù)據(jù)是否真的發(fā)生變化

繼續(xù)向下分析,mBackupFile.exists() 方法判斷當(dāng)前是否存在備份文件,如果不存在,則將原始文件重名為備份文件。此時如果存在該文件的備份文件,則直接將源文件丟棄:mFile.delete()。

writeToFile 方法的下半部分分析,如下圖:

writeToFile 方法下半部分

由于代碼篇幅較長,省去部分。

創(chuàng)建 mFile 文件的輸出流,這里很明白是要寫入數(shù)據(jù)使用,系統(tǒng)將真正寫入數(shù)據(jù)的操作都封裝在 XmlUtils 中,然后強(qiáng)制 sync 落盤到閃存。

強(qiáng)制落盤

熟悉 I/O 的朋友都知道,我們應(yīng)用程序平時用到的 read/write 操作都屬于標(biāo)準(zhǔn) I/O,也就是緩存 I/O(Buffered I/O)。它的關(guān)鍵特性有:

(1)對于讀操作來說,當(dāng)應(yīng)用程序讀取某塊數(shù)據(jù)的時候,如果這塊數(shù)據(jù)已經(jīng)存放在頁緩沖中,那么這塊數(shù)據(jù)就可以立即返回給應(yīng)用程序,而不需要經(jīng)過實際的物理讀盤操作。

(2)對于寫操作來說,應(yīng)用程序也會將數(shù)據(jù)先寫到頁緩沖(Page Cache)中去,數(shù)據(jù)是否被立即寫到磁盤上去取決于應(yīng)用所采用寫操作的機(jī)制。默認(rèn)系統(tǒng)采用的是延遲寫機(jī)制,應(yīng)用程序只需要將數(shù)據(jù)寫到頁緩沖中去就可以了,完全不需要等數(shù)據(jù)全部被寫回到磁盤,系統(tǒng)會負(fù)責(zé)定期地將放在頁緩沖中的數(shù)據(jù)刷到磁盤上。

SharedPreferences 在寫入文件時采用強(qiáng)制落盤機(jī)制來保證數(shù)據(jù) “不丟失”:FileUtils.sync()。

如果上面步驟沒有發(fā)生任何異常,則刪除備份文件,還記得前面說過,在新的寫入文件之前,先將原始文件備份嗎?如下圖:

原文件重名為備份文件

如果寫入過程未發(fā)生異常,則直接 return,表示本次寫入成功。如果寫入過程發(fā)生異常,則直接將源文件刪除:mFile.delete()。catch() 異常后的代碼調(diào)用,刪除源文件 mFile.delete(),如下圖:

刪除 SharedPreference 的原文件

此時不知道會不會有這樣一個疑問?數(shù)據(jù)都丟失了?

讓我們再來看下 ShardPreferencesImpl 構(gòu)造方法(源碼上圖已貼出)的最后 startLoadFromDisk() 方法,如下圖:(只貼出與 Backup 文件相關(guān))

loadFromDisk 加載文件數(shù)據(jù)到內(nèi)存

檢查源文件的備份文件是否存在:mBackupFile.exists(),如果存在,則將源文件刪除:mFile.delete(),然后將備份文件修改為源文件:mBackupFile.renameTo(mFile)。后續(xù)操作就是從備份文件加載相關(guān)數(shù)據(jù)到內(nèi)存 mMap 容器中了。

小結(jié)

SharedPreferences 的寫入操作,首先是將源文件備份:mFile.renameTo(mBackupFile) 再寫入所有數(shù)據(jù),只有寫入成功,并且通過 sync 完成落盤后,才會將 Backup(.bak) 文件刪除。如果寫入過程中進(jìn)程被殺,或者關(guān)機(jī)等非正常情況發(fā)生。進(jìn)程再次啟動后如果發(fā)現(xiàn)該 SharedPreferences 存在 Backup 文件,就將 Backup 文件重名為源文件,原本未完成寫入的文件就直接丟棄,這樣最多也就是未完成寫入的數(shù)據(jù)丟失,它能保證最后一次落盤(真正落盤)成功后的數(shù)據(jù)。也正式這個 BackUp 機(jī)制,導(dǎo)致多進(jìn)程可能會丟失新寫入的數(shù)據(jù)。但也不是只有多進(jìn)程場景才會發(fā)生數(shù)據(jù)丟失的情況。

1、Context.MODE_MULTI_PROCESS 到底做了什么?

在《Android之不要濫用SharedPreferences》只是簡單給大家提到:使用 Context.MODE_MULTI_PROCESS 只是重新從文件加載了一遍 SharedPreferences 數(shù)據(jù),不要指望這貨能夠跨進(jìn)程通信。如下圖:

MODE_MULTI_PROCESS 的作用

關(guān)于 SharedPreferences 的創(chuàng)建過程在上篇文章中已經(jīng)做過詳細(xì)介紹,不再贅述,這里主要關(guān)注紅線框中部分:startReloadIfChangedUnexpectedly 方法跟蹤:

重新從文件中加載一遍到內(nèi)存 Map

hasFileChangedUnexpectedly 方法如果返回 false 直接 return。
否則 startLoadFromDisk(關(guān)于 startLoadFromDisk 方法的作用已經(jīng)多次說明過,不再贅述)。hasFileChangedUnexpectedly 方法如下圖:

當(dāng)前文件內(nèi)容是否發(fā)生過變化

SharedPreferences 中會記錄最后修改時間以及文件大小,當(dāng)使用 Context.MODE_MULTI_PROCESS 時,此時會通過 StructStat(Os.stat() 返回) 計算得到,然后與當(dāng)前最后同步時間和文件大小進(jìn)行比較,如果不匹配就會觸發(fā) startLoadFromDisk 方法執(zhí)行,既重新加載文件內(nèi)容到內(nèi)存 mMap 中。

2、SharedPreferences 的監(jiān)控

SharedPreferences 中為我們提供了 OnSharedPreferenceChangeListener 數(shù)據(jù)改變回調(diào):

數(shù)據(jù)改變通知

需要注意 onSharedPreferenceChanged() 的回調(diào)時機(jī)在 commit() 和 apply() 有所區(qū)別:

(1)使用 commit() 提交時,onSharedPreferenceChanged() 回調(diào)時機(jī)是在數(shù)據(jù)落盤完成之后(不代表一定成功,有可能發(fā)生異常)

(2)使用 apply() 提交時,onSharedPreferenceChanged() 回調(diào)時機(jī)是在完成數(shù)據(jù)內(nèi)存替換之后,既 mModified 中數(shù)據(jù)提交到 mMap 完成之后(前者是對我們一系列putXxx() 或 remove() 做保存,后者是寫入文件時使用)。

(3)系統(tǒng)保存 OnSharedPreferenceChangeListener 對象在 WeakHashMap 中:

弱引用保存數(shù)據(jù)監(jiān)聽

不熟悉 WeakHashMap 的機(jī)制可以去了解下,故如果在局部創(chuàng)建 OnSharedPreferenceChangeListener 對象,在方法體結(jié)束后生命周期即結(jié)束。

通過 OnSharedPreferenceChangeListener 回調(diào)我們可以監(jiān)控任意 SharedPreferences 提交的 key:value,比如較大的數(shù)據(jù)直接給出警告;也可以監(jiān)控單個 SharedPreferences 文件是否過大。

  • SharedPrefenerces 的優(yōu)化

我們也可以替換通過復(fù)寫 Application 的 getSharedPreferences 方法替換系統(tǒng)默認(rèn)實現(xiàn),比如優(yōu)化卡頓、合并多次 apply 操作、支持跨進(jìn)程操作等。具體如何實現(xiàn)參考這里

重寫 Application 相關(guān)方法替換 SharedPreferences 實現(xiàn)

對系統(tǒng)提供的 SharedPreferences 的小修小補(bǔ)雖然性能有所提升,但是依然不能徹底解決問題。基本每個大公司都會自研一套替代的存儲方案,比如微信最近就開源了MMKV

最后

SharedPreferences 是我們?nèi)粘=?jīng)常使用的存儲方法,但是里面的確會有大大小小的暗坑。所以我們需要充分了解它們的優(yōu)缺點,這樣在工作中可以更好地使用和優(yōu)化。

總的來說,我們需要結(jié)合應(yīng)用場景選擇合適的數(shù)據(jù)存儲方法。除了 SharedPreferences,Android 還為應(yīng)用開發(fā)者提供了其它存儲數(shù)據(jù)的方法。你可以參考 Android 存儲優(yōu)化系列專題中其他存儲方法分析。

文中分析如有不妥或更好的分析結(jié)果,還請大家指出!如果你喜歡我的文章,就請留個贊吧!

推薦閱讀

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

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

  • ORA-00001: 違反唯一約束條件 (.) 錯誤說明:當(dāng)在唯一索引所對應(yīng)的列上鍵入重復(fù)值時,會觸發(fā)此異常。 O...
    我想起個好名字閱讀 5,446評論 0 9
  • 我們經(jīng)常用SharedPreferences用來存儲一些比較小的鍵值對集合,適合保存應(yīng)用的配置參數(shù), 我們將會帶著...
    簡書汪閱讀 1,148評論 1 0
  • 任何一個應(yīng)用程序,其實說白了就是在不停地和數(shù)據(jù)打交道,我們聊QQ、看新聞、刷微博,所關(guān)心的都是里面的數(shù)據(jù),...
    AndYMJ閱讀 1,736評論 2 5
  • 今天看到一位朋友寫的mysql筆記總結(jié),覺得寫的很詳細(xì)很用心,這里轉(zhuǎn)載一下,供大家參考下,也希望大家能關(guān)注他原文地...
    信仰與初衷閱讀 4,759評論 0 30
  • 3月29日,人民日報轉(zhuǎn)載了一篇名為《手機(jī)游戲不能顛覆歷史》的文章后,網(wǎng)上關(guān)于王者榮耀的評論隨之發(fā)酵。 相關(guān)數(shù)據(jù)顯示...
    藍(lán)顆粒閱讀 12,871評論 271 241