Android之不要濫用SharedPreferences

閃存

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

SharedPreferences 系列

Android 之不要濫用 SharedPreferences

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

ContentProvider 系列(待更)

Android 存儲(chǔ)選項(xiàng)之 ContentProvider 啟動(dòng)過程源碼分析

《Android 存儲(chǔ)選項(xiàng)之 ContentProvider 深入分析》

對(duì)象序列化系列

Android 對(duì)象序列化之你不知道的 Serializable

Android 對(duì)象序列化之 Parcelable 取代 Serializable ?

Android 對(duì)象序列化之追求完美的 Serial

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

《Android 數(shù)據(jù)序列化之 JSON》

《Android 數(shù)據(jù)序列化之 Protocol Buffer 使用》

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

SQLite 存儲(chǔ)系列

Android 存儲(chǔ)選項(xiàng)之 SQLiteDatabase 創(chuàng)建過程源碼分析

Android 存儲(chǔ)選項(xiàng)之 SQLiteDatabase 源碼分析

數(shù)據(jù)庫(kù)連接池 SQLiteConnectionPool 源碼分析

SQLiteDatabase 啟用事務(wù)源碼分析

SQLite 數(shù)據(jù)庫(kù) WAL 模式工作原理簡(jiǎn)介

SQLite 數(shù)據(jù)庫(kù)鎖機(jī)制與事務(wù)簡(jiǎn)介

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


前言

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

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

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

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

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

通過Context的getSharedPreferences方法得到Sp對(duì)象。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

commitToMemory():

接下面:

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

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

此時(shí)又回到commit方法:

調(diào)用enqueueDiskWirte:

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

接著向下:

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

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

使用apply進(jìn)行提交:

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

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

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

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

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

我們看get操作做了哪些:

也就是SharedPreferencesImpl的get操作:

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

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

二、apply一定安全嗎?

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

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

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

ActivityThread.java:

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

你沒有看錯(cuò)又要等待,等待什么呢?

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

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

QueuedWork.java:

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

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

三、結(jié)論

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

四、正確使用的建議

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

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

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

4、不要每次都edit,因?yàn)槊看味紩?huì)創(chuàng)建一個(gè)新的EditorImpl對(duì)象,最好是批量處理統(tǒng)一提交。

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

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

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

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

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

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

推薦閱讀

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

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

其他系列專題

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

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

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

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

  • 一.sp是什么?能做什么? SharedPreferences(簡(jiǎn)稱SP)是Android中很常用的數(shù)據(jù)存儲(chǔ)方式,...
    lemonCode閱讀 862評(píng)論 0 2
  • Android上常見的數(shù)據(jù)存儲(chǔ)方式有哪些呢? SharedPreferences這種存儲(chǔ)數(shù)據(jù)的方式我們平時(shí)用的都對(duì)...
    編程小豬閱讀 4,606評(píng)論 0 5
  • Android 五種數(shù)據(jù)存儲(chǔ)的方式分別為: SharedPreferences:以Map形式存放簡(jiǎn)單的配置參數(shù); ...
    ghroost閱讀 12,654評(píng)論 0 23
  • 某一瞬間找到了一點(diǎn)光,能引燃所謂小宇宙的那星星之火。也只有在文字中才能感受到存在的價(jià)值,因?yàn)樵诂F(xiàn)有的價(jià)值觀體系中,...
    也就朝夕閱讀 267評(píng)論 0 0
  • 小歸:小歸歸于心 今年年初因?yàn)閰⒓恿斯沧x群活動(dòng),進(jìn)入了輕松讀書的核心fans群,在里面認(rèn)識(shí)了許多書友和牛人。其中有...
    小歸ing閱讀 651評(píng)論 3 5