之前一直沒有讀源碼的習慣,直到今年年初才開始慢慢養成多看源碼的習慣,不過之前在使用 SharedPreferences 時總覺得為啥要通過 Editor 去寫,而在獲取寫入的值的時候卻用 SharedPreferences 直接讀出來,這次看過源碼之后才知道是為什么。
說到 SharedPreferences,我們肯定想到的是它為我們提供簡單數據的存儲功能,因為 Android 本身支持多種類型的數據的持久化,比如文件、數據庫、SharedPreferences 等等,而如果只是存出一些簡單的數據類型,那么 SharedPreferences 是不錯的選擇,它的存儲形式是基于 xml 的,通過鍵值對的方式保存數據,而且數據可以是私有的。
當然 SharedPreferences 的使用很簡單,我們先獲取 SharedPreferences 實例,然后通過 Editor 保存和提交數據,而讀取的時候可以直接通過 SharedPreferences 的各種 getxxx 的方式讀取。
SharedPreferences 本身是接口,里面還有一個 Editor 的接口,它的實現類是 SharedPreferencesImpl,里面實現了 SharedPreferences 各種方法,還是實現了 Editor 接口。
先來說說 SharedPreferences 的獲取,有兩種方式:
1.通過 PreferenceManager 獲取。
2.通過 Context 中的 getSharedPreferences() 獲取。
其實第一種也只是封裝了一下 Context 中的 getSharedPreferences()。
這里有一點需要注意,我們應該減少 SharedPreferences 的大小,因為它本質是以 xml 存在本地,如果 SharedPreferences 數據量過大,那么初始化 SharedPreferences 時,會減慢讀取速度。
這次的源碼閱讀只閱讀了部分,幫我搞清了一些方法,但并沒有通讀 SharedPreferences 源碼,下面就來分享一下我的收獲。
先來說說 SharedPreferences 的存儲形式,我一開始只是認為 SharedPreferences 將值存入磁盤中,而其實它分為兩部分,首先它會先存在內存中一次,然后再提交到磁盤中。
截圖中的 mMap 的作用有兩個,一個是初始化的時候把磁盤中讀到的數據賦值給它,然后我們再次提交的新提交的部分時,也會直接存到這里,這也就是先存到內存中一次。
在 SharedPreferences 構造中,會調用 startLoadFromDisk(),這個方法會新起一個線程,執行 loadFromDisk(),這個方法就是讀取磁盤中 xml 文件,然后將讀取到的值賦給 mMap。
那么接下來來看看我們在 Editor 中提交的時候,代碼都做了什么。
SharedPreferencesImpl 有一個內部類 EditorImpl,它實現了 SharedPreferences 中的定義的 Editor 接口,然后當我們獲取 Editor 時,就是得到了一個 EditorImpl 實例。
EditorImpl 中的 mModified 是用來存儲本次想要提交的內容,各種 putxxx 這里就不在多說了,看看兩個重要的提交方式,一個是 commit(),另一個是 apply()。
官方文檔中也明確的說明了這兩種方式的明確區別,除了返回值,還有提交到磁盤的方式,commit() 是同步,而 apply() 是異步。
但無論提交到磁盤的方式如何,它們都會首先將本次想要提交的內容通過 commitToMemory() 存到內存中,也就是存到 SharedPreferencesImpl 的 mMap 中。通過 MemoryCommitResult 拿到提交內容的內存的結果,然后再將拿到的結果寫入磁盤。
final MemoryCommitResult mcr = commitToMemory();
寫到磁盤時,都是通過調用 SharedPreferencesImpl 的 enqueueDiskWrite() 來完成的,apply() 調用 enqueueDiskWrite() 時會傳入一個實例化的 Runnable,而 commit() 則傳 null 值過去,enqueueDiskWrite() 通過傳遞過來 Runnable 是否為 null,來判斷是否以什么方式提交。
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
final boolean isFromSyncCommit = (postWriteRunnable == null);
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
這就是 enqueueDiskWrite() 的源碼,如果傳遞過來的 postWriteRunnable 為 null 則說明同步寫入到磁盤中,而非 null 時利用單線程的線程池來將結果寫入到磁盤中。
至此一個基本上寫入的流程就完成了。
那些讀的時候就直接調用 SharedPreferencesImpl 中的 getxxx 就可以了,而這些方法會線程安全的從 mMap 中拿到我們需要的值。
SharedPreferences 源碼基本上就讀到了這里,基本的一些操作也都梳理清楚,接下來說幾點使用時需要注意的地方。
1.單個 SharedPreferences 文件不宜過大,所以最好不使用默認的存儲目錄,而是根據需求,自己定義存儲的文件名,如果單個 xml 文件過大,那么初始化時會影響讀取速度。
2.SharedPreferences 是進程內單例,當然它也可以讀到別人進程寫入的內容,但是會有些小問題,所以跨進程使用 SharedPreferences 還有很多需要注意的地方。
3.提交時如果不需要返回結果(是否提交成功),那么直接調用 commit()。
4.單次提交內容不易過大過多,那么同步可能會阻塞,雖然也可以異步,但是異步其實也只是單線程。
5.它會同時存在磁盤中,也會存在內存中。
所以是否會存在這種問題?內容不足時,回收了 mMap,下次使用時之前的存過的值會返回 null 或者默認值了呢?
如果真的會的話就要重新再去實例化一個 SharedPreferences 來使用了。
以及內容就是本次閱讀心得,僅供參考,如果問題,可以留言溝通。