Android之不要濫用SharedPreferences

閃存

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

SharedPreferences 系列

Android 之不要濫用 SharedPreferences

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

ContentProvider 系列(待更)

Android 存儲選項之 ContentProvider 啟動過程源碼分析

《Android 存儲選項之 ContentProvider 深入分析》

對象序列化系列

Android 對象序列化之你不知道的 Serializable

Android 對象序列化之 Parcelable 取代 Serializable ?

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ù)庫鎖機制與事務(wù)簡介

SQLite 數(shù)據(jù)庫優(yōu)化那些事兒


前言

本文不是與大家一起探討SharedPreferences的基本使用,而是結(jié)合源碼的角度揭秘對SharedPreference使用不當(dāng)引發(fā)的嚴重后果以及該如何正確使用。

SharedPreferences是Android平臺上一個輕量級的存儲輔助類,用來保存應(yīng)用的一些常用配置,它提供了string,set,int,long,float,boolean六種數(shù)據(jù)類型。最終數(shù)據(jù)是以xml形式進行存儲。在應(yīng)用中通常做一些簡單數(shù)據(jù)的持久化緩存。SharedPreferences作為一個輕量級存儲,所以就限制了它的使用場景,如果對它使用不當(dāng)將會帶來嚴重的后果。

一、從源碼的角度出發(fā)

1、SharedPreferences的創(chuàng)建過程

后面統(tǒng)一簡稱:Sp

通過Context的getSharedPreferences方法得到Sp對象。

這里實際調(diào)用了ContextImpl的getSharedPreferences()。

從源碼可以看到:首先在sSharedPrefs中獲取Sp對象,那這個sSharedPrefs是個什么東西?

sSharedPrefs實際是個Map對象,并且被聲明為static final,這就意味著我們整個應(yīng)用中只存在一個sSharedPrefs對象。如果第一次創(chuàng)建Sp對象此時肯定是獲取到的是null,緊接著進入第一個if語句getSharedPrefsFile(name),參數(shù)想必大家都猜的到:就是我們創(chuàng)建Sp時傳的的name,其實通過名字也可以看得出根據(jù)傳遞name創(chuàng)建一個File:

創(chuàng)建name.xml文件。

跟蹤到這里儲存文件的創(chuàng)建我們就找到了。

緊接著new SharedPreferencesImpl(),看下SharedPreferencesImpl的構(gòu)造方法:

實際上SharedPreferences只是個接口,而真正的實現(xiàn)是SharedPreferencesImpl,我們后續(xù)的get,put操作實際也是通過SharedPreferencesImpl對象完成的。

構(gòu)造方法最后一行:startLoadFromDisk():

從這里可以看出首先將mLoaded變量賦值為false,起到一個狀態(tài)的變化作用,在后續(xù)我們會說到這個mLoaded變量很重要(其實主要多線程等待),然后開啟一個線程loadFromDiskLocked():

代碼稍微有點長,但是并不復(fù)雜。94行 - 105行都是做一些相關(guān)的檢查。緊接著向下創(chuàng)建BufferedInputStream對象,將mFile作為參數(shù),mFile還記得嗎?它就是根據(jù)我們傳遞的name創(chuàng)建的文件。然后通過XmlUtils.readMapXml()將文件內(nèi)容寫入到map中并返回。在123行將mLoaded設(shè)置為true,代表已經(jīng)將文件里的加載完成,存儲在一個map中并且將其賦值給成員變量mMap:

說道這想必大家已經(jīng)明白:我們在Sp儲存的數(shù)據(jù)會在本地生成一個.xml文件外,還會將該文件的數(shù)據(jù)緩存在一個map對象中。如果是第一次創(chuàng)建顯然BufferedInputStream不會讀取到任何數(shù)據(jù),此時XmlUtils.readMapXml()解析返回自然為null,然后mMap = new HashMap();

然后再回到ContextImpl的getSharedPreferences方法最后:

如果Sp已經(jīng)存在了,會判斷mode否等于Context.MODE_MULTI_PROCESS,然后如果API小于11:

沒錯Context.MODE_MULTI_PROCESS僅僅是重新加載一遍數(shù)據(jù)到內(nèi)存mMap,所以指望SharedPreferences實現(xiàn)跨進程通信可以死心了。

說到這,SharedPreference的創(chuàng)建過程就算是講完了:getSharedPreferences實際返回SharedPrefenercesImpl對象,首先在sSharedPrefs容器中查找,如果未找到則創(chuàng)建Sp的對象并添加到sSharedPrefs。

2、put數(shù)據(jù)

通過上面的分析getSharedPreferences實際創(chuàng)建的是SharedPreferencesImpl對象。

此時edit自然是調(diào)用的SharedPreferencesImpl的方法:

還記得我們之前提到的mLoaded變量嗎:當(dāng)我們第一次創(chuàng)建SharedPreferences時候,會將該變量置為false,然后開啟線程將文件中的數(shù)據(jù)完成讀取進map之后再將其置為true,讀取文件的內(nèi)容到map是在工作線程,此時edit方法是在主線程,如果此時工作線程讀取時間過久,那edit方法將長時間處于等待狀態(tài)。一旦超過5秒就會發(fā)生ANR危險

調(diào)用SharedPreferencesImpl的edit方法返回的是EditorImpl對象:

我們一些列的put操作,還有clear,remove,apply,commit都是在EditorImpl對象中:

從源碼可以得知,我們一些列的put和remove之后是將數(shù)據(jù)添加進入mModifiled中,mModifiled是一個Map對象,其實從名字也可以看出代表為暫存的。clear僅修改mClear狀態(tài)。執(zhí)行操作之后必須要執(zhí)行commit:

這里需要注意的是:我們每次edit都會創(chuàng)建一個新的EditorImpl對象。接著跟蹤commit操作:

commitToMemory():

接下面:

代碼篇幅有些長,我們只關(guān)注重點部分:for循環(huán)這里,上面我們提到一系列的put和remove操作都添加進入mModified中,也就是mModified保留著我們當(dāng)前的改變,通過遍歷該容器,與mMap數(shù)據(jù)做一個比較,比如相同key但是value發(fā)生了變化此時修改mMap中的數(shù)據(jù)。然后mMap就是最后一次commit的數(shù)據(jù)。最后清空mModified容器。

方法的最后返回MemoryCommitResult,其實從名字也可以看出它的作用:標記本次提交的狀態(tài)是否發(fā)生改變并將結(jié)果返回。

此時又回到commit方法:

調(diào)用enqueueDiskWirte:

首先writeToDiskRunnable對象,在該對象的方法中執(zhí)行寫入文件操作(就是將最后一次提交之后mMap的數(shù)據(jù)寫回到文件)。

接著向下:

由于commit方法的第二個參數(shù)Runnable傳遞null,故此時siFromSyncCommit為true,可以看到執(zhí)行writeToDiskRunnable.run,直接在當(dāng)前線程(UI線程)執(zhí)行寫入文件操作。此時return。

我們在修改數(shù)據(jù)之后除了選擇commit提交之外,還可以使用apply進行提交:首先writeToDiskRunnable對象,在該對象的方法中執(zhí)行寫入文件操作(就是將最后一次提交之后mMap的數(shù)據(jù)寫回到文件)。

使用apply進行提交:

此時siFromSyncCommit等于false,此時會執(zhí)行enqueueDiskWrite方法的:

QueuedWork是一個線程池,而且只有一個核心線程,提交的任務(wù)到會加入到一個等待隊列中按照順序執(zhí)行。

那么commit發(fā)生在UI線程而apply發(fā)生在工作線程。如果保證不阻塞UI線程我們使用apply來提交修改是否就絕對安全了呢?這里先告訴大家答案:絕對不是!?。?!,后面會給大家繼續(xù)分析。

接下來我們先來看下get操作。

3、get數(shù)據(jù)

我們看get操作做了哪些:

也就是SharedPreferencesImpl的get操作:

其實通過上面的分析我們已經(jīng)得到答案:通過SharedPreferenceImpl存儲的數(shù)據(jù)都會在內(nèi)存中保留一份mMap,這里也是直接在mMap中讀取數(shù)據(jù)即可。

這里要著重說下awaitLoadedLocked方法,之前我們也提到過該方法主要是檢查mLoaded變量狀態(tài):當(dāng)我們第一次創(chuàng)建Sp對象時,它會開啟一個工作線程將指定的文件中內(nèi)容加載到mMap中,當(dāng)加載完成改變mLoaed變量狀態(tài);否則awaitLoadedLocked方法會一直等待下去。這里涉及到一個優(yōu)化點我們后續(xù)給大家總結(jié)。

二、apply一定安全嗎?

上面我們提到過確認提交數(shù)據(jù)除了commit還可以apply,apply使寫入文件操作發(fā)生在工作線程中,這樣防止IO操作阻塞UI線程;這樣真的就絕對安全嗎?答案不是的。

我們要去跟蹤另外一部分源碼:

首先Android四大組件的創(chuàng)建以及生命周期調(diào)用都是進程間通信完成的,到我們自己的進程中完成調(diào)度過渡任務(wù)的是ActivityThread,ActivityThread是我們應(yīng)用進程的入口。來看下Actvity的onStop回調(diào)過程:

ActivityThread.java:

檢查當(dāng)前 SharedPreferences 所有任務(wù)是否執(zhí)行完成,否則等待

你沒有看錯又要等待,等待什么呢?

還記得我們確認提交數(shù)據(jù)使用apply操作將寫入文件操作添加進線程池隊列中嗎?sPendingWorkFinishers就是SharedPreferencesImpl的enqueueDiskWirte方法的最后一行,當(dāng)我們使用apply時就會執(zhí)行如下添加到線程池中任務(wù)隊列

加入線程池,排隊等待執(zhí)行

QueuedWork.java:

假設(shè)我們apply非常多的任務(wù)。該線程池隊列是串行執(zhí)行,當(dāng)我們關(guān)閉Activity時:會檢查sPendingWorkFinishers隊列中任務(wù)是否已經(jīng)全部執(zhí)行完成,否則一直等到全部執(zhí)行完成。如果此時等待超過5s

由此得知 apply 也不是絕對安全的,試想當(dāng)你 apply 提交較多的任務(wù)并且都是大型 key 或 value 時。

三、結(jié)論

當(dāng)我們首次創(chuàng)建 SharedPreferences 對象時,會根據(jù)文件名將文件下內(nèi)容一次性加載到 mMap(SharedPreferencesImpl 成員) 容器中,每當(dāng)我們 edit 都會創(chuàng)建一個新的 EditorImpl 對象,當(dāng)修改或者添加數(shù)據(jù)時會將數(shù)據(jù)添加到 mModifiled (EditorImpl 成員)容器中,然后 commit 或 apply 操作比較 mMap 與 mModifiled 數(shù)據(jù)修正 mMap 中最后一次提交數(shù)據(jù),然后寫入到文件中。而 get 直接從 mMap 中讀取。試想如果此時你存儲了一些大型 key 或 value 它們會一直存儲在內(nèi)存中得不到釋放。

四、正確使用的建議

1、不要存放大的 key 和 value 在 SharedPreferences 中,否則會一直存儲在內(nèi)存中得不到釋放,內(nèi)存使用過高會頻發(fā)引發(fā)GC,導(dǎo)致界面丟幀甚至ANR。

2、不相關(guān)的配置選項最好不要放在一起,單個文件越大讀取速度則越慢。

3、讀取頻繁的 key 和不頻繁的 key 盡量不要放在一起(如果整個文件本身就較小則忽略,為了這點性能添加維護得不償失)。

4、不要每次都edit,因為每次都會創(chuàng)建一個新的EditorImpl對象,最好是批量處理統(tǒng)一提交。

否則 edit().commit 每次創(chuàng)建一個新的 EditorImpl 對象并且進行一次 I/O 操作,嚴重影響性能。

5、commit 發(fā)生在 UI 線程中,apply 發(fā)生在工作線程中,對于數(shù)據(jù)的提交最好是批量操作統(tǒng)一提交。雖然apply 發(fā)生在工作線程(不會因為IO阻塞UI線程)但是如果添加任務(wù)較多也有可能帶來其他嚴重后果(參照ActivityThread源碼中handleStopActivity方法實現(xiàn))。

6、盡量不要存放 JSON 和 HTML,這種可以直接文件緩存。

7、不要指望這貨能夠跨進程通信 Context.PROCESS 。詳情參考另外一篇《Android 之不要濫用 SharedPreferences(2)— 數(shù)據(jù)丟失

8、最好提前初始化 SharedPreferences,避免 SharedPreferences 第一次創(chuàng)建時讀取文件線程未結(jié)束而出現(xiàn)等待情況。

最后該篇文章是基于較早的 Android ?API Level 16 源碼分析,如果想要了解最新(Level 28)請參考最新一篇文章的分析。

推薦閱讀

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

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

其他系列專題

Android 之你真的了解 View.post() 原理嗎?

深入 Activity 三部曲(1)View 繪制流程之 setContentView() 到底做了什么 ?

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

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

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