Android開發中,我們經常會用到SharedPreferences,它是一種輕量的資料儲存方式,通常用來儲存一些簡單的配置資訊。看了網路上的一些文章,感覺都不是特別滿意,因此希望能結合自己的經驗和理解寫一篇分析SharedPreferences的文章。本文不會講解SharedPreferences的基本用法,而是會結合原始碼來分析SharedPreferences的工作原理,以及使用中存在的一些問題。
通過這篇文章,你可以瞭解到:
-
SharedPreferences是怎麼工作的
-
SharedPreferences使用中有哪些坑
-
怎麼來避免SharedPreferences的那些問題
首先,我們要搞清楚SharedPreferences的本質是什麼。它的本質是基於xml檔案儲存的key-value鍵值對資料,其儲存位置在/data/data/包名/shared_prefs目錄下。由於它是儲存在應用程式的私有目錄下,外部是無法直接訪問的。也就是說它實際上就是一個xml檔案,和普通的xml沒有本質區別,內容也和我們工程程式碼裡的strings.xml檔案的內容類似。
原始碼分析
下面我們通過對原始碼的分析,講解一下它的工作原理。先來看一下SharePreferences的基本用法。
SharedPreferences sp = context.getSharedPreferences(“file1”, Context.MODE_PRIVATE);
sp.edit().putBoolean(“key1”, false).commit();
sp.getBoolean(“key1”)
那麼我們就從getSharedPreferences()方法開始講起,實際上Context最終呼叫的是ContextImpl中的getSharedPreferences方法,我們看下這個方法。
其中包含一個mSharedPrefsPaths物件,它是ArrayMap型別,我們可以在App中建立多個sp檔案,mSharedPrefsPaths中就是儲存了不同sp檔名和sp檔案的對應關係。這裡的getSharedPreferencesPath方法實際上就是在磁碟上建立了一個xml檔案。檢視上圖最後一行的getSharedPreferences方法。
我們看到這個方法實際返回了一個SharedPreferencesImpl物件,看下SharedPreferencesImpl的構造方法。
其中呼叫了startLoadFromDisk方法,startLoadFromDisk在子執行緒裡呼叫了loadFromDisk,執行執行緒之前將mLoaded設定為false,再來看下這個loadFromDisk方法。
這個方法的程式碼很多,我們只看最核心的部分,它通過XmlUtils.readMapXml()將檔案讀取到mMap中,mMap是一個HashMap,並且將mLoaded設定為true,大家記住mLoaded這個變數,後面還會遇到它。也就是說,sp檔案的內容被讀取到記憶體並且快取到mMap中了,後續對sp的操作都與記憶體中的快取有關。既然sp檔案的內容會快取到記憶體中,如果檔案中儲存了大量資料,就會佔用很大的記憶體空間,這點需要特別注意。
SharedPreferences的建立過程講完了,下面我們來看一下put過程。put操作首先要呼叫edit()方法,
又見到了mLoaded這個變數,我們回憶一下之前的邏輯,在開始開啟執行緒讀取sp檔案到記憶體的時候,這個變數被置為false,等執行緒執行完會置為true,在上圖的awaitLoadedLocked方法中,如果發現mLoaded為false,則呼叫wait方法,此時會阻塞當前執行緒,直到sp檔案讀取完成,才呼叫notifyAll()通知這裡被阻塞的執行緒繼續執行。也就是說,如果讀取sp檔案的操作執行時間很長的話,這裡就可能會阻塞主執行緒導致ANR。
怎麼才能儘可能的避免這個問題呢?首先,我們需要將sp檔案根據功能和特點分解為多個小檔案,比如根據不同的功能模組進行劃分,或者根據讀寫的頻率,也可以根據是否App啟動的時候就需要載入。如果每個檔案足夠小,那麼在讀取檔案到記憶體的時候,耗時自然也就少了。尤其是在App啟動的時候,只需要載入啟動時需要的sp配置,可以一定程度上減少啟動時間。
下面繼續看原始碼。edit()方法返回了一個Editor物件,實際的型別是EditorImpl。
EditorImpl中包含一個HashMap型別成員mModified,呼叫Editor的方法如putString之後,都只是將資料儲存在mModified中。這裡只是資料的暫存區,因此如果忘記呼叫commit或者apply方法,資料其實並沒有寫入磁碟。有一點需要注意的是我們每次呼叫edit方法,都會建立一個mModified物件,因此,有必要減少edit方法的呼叫。
最後,就是呼叫commit或者apply方法了。我們知道commit是同步寫入,會返回執行結果;而apply方法是非同步寫入,並不會返回執行結果。下面通過原始碼來分析下它們的實現。
commit方法中先後呼叫了commitToMemory和enqueueDiskWrite。commitToMemory方法的作用是將前面提到的mModified中快取的資料更新到前面提到的mMap中,這個mMap會被最終寫入檔案。我們看enqueueDiskWrite方法,它的第二個引數傳了null,因此,isFromSyncCommit為true,然後直接執行了writeToDiskRunnable.run()方法,其中通過呼叫writeToFile將mMap中的配置內容寫入sp檔案。
從原始碼中我們可以看出,commit的執行是同步的,而且是全量的寫入。如果不是必要的情況,儘量不要使用commit去儲存sp的配置,以防止寫檔案阻塞主執行緒。
我們再來看apply方法的實現有什麼不同。
這裡所不同的是enqueueDiskWrite的第二個引數不為null,所以方法內部將寫入檔案的操作放入了單執行緒的執行緒池非同步執行:
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable)。
由於是單執行緒,來不及執行的Runnable都被放在佇列中等待執行 。writeToDiskRunnable裡面執行了writeToFile將sp寫入檔案,然後呼叫了postWriteRunnable的run()方法,這裡面又呼叫了awaitCommit的run()方法,最後呼叫了mcr.writtenToDiskLatch.await()。那麼這個writtenToDiskLatch又有什麼作用呢?通過程式碼,我們發現writeToFile方法裡面最終會呼叫writtenToDiskLatch的countDown方法,也就是說,如果sp檔案的寫入一直沒有執行完,writtenToDiskLatch.await()這個呼叫就會阻塞在這裡,但從實際的執行時序上來看writtenToDiskLatch的countDown的呼叫又肯定是在await之前的,那麼這個await的呼叫到底有什麼作用呢?我們又注意到,這裡有一行程式碼:QueuedWork.add(awaitCommit)。 我們看下這個QueuedWork是什麼?
圖中略去了部分程式碼,add方法實際上就是將runnable加入到一個ConcurrentLinkedQueue中。下面的waitToFinish方法裡會去遍歷queue中的每個Runnable,並執行它的run方法。那麼waitToFinish方法又是在哪裡呼叫的呢?我們根據註釋找到了ActivityThread類的handlePauseActivity、handleStopActivity方法,我們來看其中的一個。
我們看到,在Activity呼叫onStop的時候,會呼叫QueuedWork.waitToFinish(),遍歷執行其中的runnable。假設我們頻繁的呼叫了apply方法,並緊接著呼叫了onStop,那麼就可能會發生onStop一直等待QueuedWork.waitToFinish執行完成而產生ANR。也就是說,即使是呼叫了apply方法去非同步提交,也不是完全安全的。如果apply方法使用不當,也許會遇到與下圖類似的問題。
上面講了put操作,由於get操作相對簡單一些,這裡就不單獨分析了。
總結
從上面的分析我們發現SharedPreferences的使用並不是那麼簡單的,使用不當可能會導致程式異常,我們對上面提到的一些問題進行一下總結:
-
sp配置不要全部都寫在一個檔案中,這樣不僅第一次載入會很慢,也會佔用大量記憶體。最好是根據一定規則分成多個sp檔案。比如頻繁和不頻繁寫入的配置就分別儲存在兩個不同的檔案中。
-
sp檔案的寫入是全量寫入,即使改了一條配置,寫入的時候也會對整個檔案進行操作,因此最好能批量操作,不要每次都commit。
-
啟動的時候需要讀取sp的配置最好非同步進行,如果一定要同步讀取,啟動的sp檔案要儘可能的小。
-
不要將太大的配置項(包括key和value)儲存在sp中,否則會佔用大量記憶體。
-
獲取SharedPreferences物件的時候會讀取sp檔案,如果檔案沒有讀取完,就執行了get和put操作,可能會出現需要等待的情況,因此最好提前獲取SharedPreferences物件。
-
每次呼叫edit方法都會建立一個新的EditorImpl物件,不要頻繁呼叫edit方法。
-
apply方法雖然是線上程中非同步將配置寫入檔案,但是如果任務很多,而且每個任務執行時間很長,也可能會導致Activity或Service在stop的時候出現ANR。
歡迎關注我的微信公眾號,收到最新的推送文章