天天看點

Android性能優化之SharedPreferences,面試阿裡巴巴學習路線+知識梳理

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

《Android 對象序列化之追求完美的 Serial》

資料序列化系列(待更)

《Android 資料序列化之 JSON》

《Android 資料序列化之 Protocol Buffer 使用》

《Android 資料序列化之 Protocol Buffer 源碼分析》

SQLite 存儲系列

《Android 存儲選項之 SQLiteDatabase 建立過程源碼分析》

《Android 存儲選項之 SQLiteDatabase 源碼分析》

《資料庫連接配接池 SQLiteConnectionPool 源碼分析》

《SQLiteDatabase 啟用事務源碼分析》

《SQLite 資料庫 WAL 模式工作原理簡介》

《SQLite 資料庫鎖機制與事務簡介》

《SQLite 資料庫優化那些事兒》

前言

本文不是與大家一起探讨SharedPreferences的基本使用,而是結合源碼的角度揭秘對SharedPreference使用不當引發的嚴重後果以及該如何正确使用。

SharedPreferences是Android平台上一個輕量級的存儲輔助類,用來儲存應用的一些常用配置,它提供了string,set,int,long,float,boolean六種資料類型。最終資料是以xml形式進行存儲。在應用中通常做一些簡單資料的持久化緩存。SharedPreferences作為一個輕量級存儲,是以就限制了它的使用場景,如果對它使用不當将會帶來嚴重的後果。

一、從源碼的角度出發

1、SharedPreferences的建立過程

後面統一簡稱:Sp

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

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-VZ1vclrU-1631005761052)(//upload-images.jianshu.io/upload_images/2573196-aea230d5c1d30521.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]

這裡實際調用了ContextImpl的getSharedPreferences()。

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-ilACSZZB-1631005761054)(//upload-images.jianshu.io/upload_images/2573196-6ef57f634c6766a0.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]

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

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-5po5dLL2-1631005761055)(//upload-images.jianshu.io/upload_images/2573196-b8885d3ce8a29d0b.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]

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

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-2SSEJyIH-1631005761057)(//upload-images.jianshu.io/upload_images/2573196-92ad04de06ae4812.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]

建立name.xml檔案。

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-6ZS0x7Ee-1631005761058)(//upload-images.jianshu.io/upload_images/2573196-2fd07d64877dc0a2.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]

跟蹤到這裡儲存檔案的建立我們就找到了。

緊接着new SharedPreferencesImpl(),看下SharedPreferencesImpl的構造方法:

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-rq793YwC-1631005761059)(//upload-images.jianshu.io/upload_images/2573196-6b5cd4310da36b29.png?imageMogr2/auto-orient/strip|imageView2/2/w/1084/format/webp)]

實際上SharedPreferences隻是個接口,而真正的實作是SharedPreferencesImpl,我們後續的get,put操作實際也是通過SharedPreferencesImpl對象完成的。

構造方法最後一行:startLoadFromDisk():

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-NJbNnZEl-1631005761060)(//upload-images.jianshu.io/upload_images/2573196-8d63fca5137918b3.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]

從這裡可以看出首先将mLoaded變量指派為false,起到一個狀态的變化作用,在後續我們會說到這個mLoaded變量很重要(其實主要多線程等待),然後開啟一個線程loadFromDiskLocked():

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-Q1Joqot3-1631005761061)(//upload-images.jianshu.io/upload_images/2573196-117ae9838e945575.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]

代碼稍微有點長,但是并不複雜。94行 - 105行都是做一些相關的檢查。緊接着向下建立BufferedInputStream對象,将mFile作為參數,mFile還記得嗎?它就是根據我們傳遞的name建立的檔案。然後通過XmlUtils.readMapXml()将檔案内容寫入到map中并傳回。在123行将mLoaded設定為true,代表已經将檔案裡的加載完成,存儲在一個map中并且将其指派給成員變量mMap:

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-xhf6KFP9-1631005761062)(//upload-images.jianshu.io/upload_images/2573196-493eab8e97ba6e44.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]

說道這想必大家已經明白:我們在Sp儲存的資料會在本地生成一個.xml檔案外,還會将該檔案的資料緩存在一個map對象中。如果是第一次建立顯然BufferedInputStream不會讀取到任何資料,此時XmlUtils.readMapXml()解析傳回自然為null,然後mMap = new HashMap();

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

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-o2HHJSap-1631005761062)(//upload-images.jianshu.io/upload_images/2573196-14c749d71cfe775c.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]

如果Sp已經存在了,會判斷mode否等于Context.MODE_MULTI_PROCESS,然後如果API小于11:

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-VuZS1Kiu-1631005761063)(//upload-images.jianshu.io/upload_images/2573196-25085bfee135b3ce.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]

沒錯Context.MODE_MULTI_PROCESS僅僅是重新加載一遍資料到記憶體mMap,是以指望SharedPreferences實作跨程序通信可以死心了。

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

2、put資料

通過上面的分析getSharedPreferences實際建立的是SharedPreferencesImpl對象。

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-Bv94g5JF-1631005761064)(//upload-images.jianshu.io/upload_images/2573196-8e263b125752baca.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]

此時edit自然是調用的SharedPreferencesImpl的方法:

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-gQECRIog-1631005761064)(//upload-images.jianshu.io/upload_images/2573196-36c17773497dc854.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-gmc9cU8Y-1631005761065)(//upload-images.jianshu.io/upload_images/2573196-11b47ff1c97d2980.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]

還記得我們之前提到的mLoaded變量嗎:當我們第一次建立SharedPreferences時候,會将該變量置為false,然後開啟線程将檔案中的資料完成讀取進map之後再将其置為true,讀取檔案的内容到map是在工作線程,此時edit方法是在主線程,如果此時工作線程讀取時間過久,那edit方法将長時間處于等待狀态。一旦超過5秒就會發生ANR危險。

調用SharedPreferencesImpl的edit方法傳回的是EditorImpl對象:

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-YPUQXrrG-1631005761065)(//upload-images.jianshu.io/upload_images/2573196-a8bc56fb029c3373.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]

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

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-cGbiK5WQ-1631005761066)(//upload-images.jianshu.io/upload_images/2573196-69e686a2312a95ac.png?imageMogr2/auto-orient/strip|imageView2/2/w/1078/format/webp)]

從源碼可以得知,我們一些列的put和remove之後是将資料添加進入mModifiled中,mModifiled是一個Map對象,其實從名字也可以看出代表為暫存的。clear僅修改mClear狀态。執行操作之後必須要執行commit:

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-MGNXsYNd-1631005761067)(//upload-images.jianshu.io/upload_images/2573196-07023a2501461744.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]

這裡需要注意的是:我們每次edit都會建立一個新的EditorImpl對象。接着跟蹤commit操作:

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-QmMeOKp2-1631005761067)(//upload-images.jianshu.io/upload_images/2573196-d5ee4de4ccf7a656.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]

commitToMemory():

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-d5veWVIE-1631005761068)(//upload-images.jianshu.io/upload_images/2573196-0fe9970d33357ce9.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]

接下面:

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-vzZ84S5k-1631005761069)(//upload-images.jianshu.io/upload_images/2573196-30aa7dbc20c1a636.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]

代碼篇幅有些長,我們隻關注重點部分:for循環這裡,上面我們提到一系列的put和remove操作都添加進入mModified中,也就是mModified保留着我們目前的改變,通過周遊該容器,與mMap資料做一個比較,比如相同key但是value發生了變化此時修改mMap中的資料。然後mMap就是最後一次commit的資料。最後清空mModified容器。

方法的最後傳回MemoryCommitResult,其實從名字也可以看出它的作用:标記本次送出的狀态是否發生改變并将結果傳回。

此時又回到commit方法:

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-XqQucNTX-1631005761069)(//upload-images.jianshu.io/upload_images/2573196-430a283b6f710441.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]

調用enqueueDiskWirte:

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-d5uny1Pc-1631005761070)(//upload-images.jianshu.io/upload_images/2573196-9e39eeba2931638d.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]

首先writeToDiskRunnable對象,在該對象的方法中執行寫入檔案操作(就是将最後一次送出之後mMap的資料寫回到檔案)。

接着向下:

由于commit方法的第二個參數Runnable傳遞null,故此時siFromSyncCommit為true,可以看到執行writeToDiskRunnable.run,直接在目前線程(UI線程)執行寫入檔案操作。此時return。

我們在修改資料之後除了選擇commit送出之外,還可以使用apply進行送出:首先writeToDiskRunnable對象,在該對象的方法中執行寫入檔案操作(就是将最後一次送出之後mMap的資料寫回到檔案)。

使用apply進行送出:

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-bzn7i2Wn-1631005761070)(//upload-images.jianshu.io/upload_images/2573196-f702fbd753adfb1e.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]

此時siFromSyncCommit等于false,此時會執行enqueueDiskWrite方法的:

[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-3x3JxFkc-1631005761071)(//upload-images.jianshu.io/upload_images/2573196-93a1f241e4e9f860.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]

QueuedWork是一個線程池,而且隻有一個核心線程,送出的任務到會加入到一個等待隊列中按照順序執行。

那麼commit發生在UI線程而apply發生在工作線程。如果保證不阻塞UI線程我們使用apply來送出修改是否就絕對安全了呢?這裡先告訴大家答案:絕對不是!!!!,後面會給大家繼續分析。

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

3、get資料

學習路線+知識梳理

花了很長時間,就為了整理這張詳細的知識路線腦圖。當然由于時間有限、能力也都有限,畢竟嵌入式全體系實在太龐大了,包括我那做嵌入式的同學,也不可能什麼都懂,有些東西可能沒覆寫到,不足之處,還希望小夥伴們一起交流補充,一起完善進步。

Android性能優化之SharedPreferences,面試阿裡巴巴學習路線+知識梳理
本文在CodeChina開源項目:Android開發不會這些?如何面試拿高薪 中已收錄,裡面包含不同方向的自學程式設計路線、面試題集合/面經、及系列技術文章等,資源持續更新中…

img-eWsEAE9Y-1631005761072)]

本文在CodeChina開源項目:Android開發不會這些?如何面試拿高薪 中已收錄,裡面包含不同方向的自學程式設計路線、面試題集合/面經、及系列技術文章等,資源持續更新中…

這次就分享到這裡吧,下篇見。