SharedPreferences真正實現的類是:SharedPreferencesImpl
構造函數中:
會創建出該XML的文件,以及一個bak的備份文件。并且調用startLoadFromDisk
開啟一個線程從本地文件中讀取配置文件。
其中每個SharedPreferencesImpl中都會有一個mLoaded
變量,它標志著是否已經從本地文件中讀取過了。而對它進行同步的時候,都是通過SharedPreferencesImpl.this
來上鎖進行的。
在loadFromDiskLocked函數中:
- 檢測.bak備份文件是否存在,如果存在的話,那么則將原來的文件刪除,然后將.bak文件renameto正常文件,也就意味著,如果在寫的時候,出問題了,導致中斷了,就使用原來沒問題的備份文件。
- 接著會通過Native檢查文件是否存在,并且進行權限檢查,看該文件是否可讀
- 通過XmlUtils.readMapXml讀取對應的XML文件,然后將數據放到Map中保存
- 讀取完后,將mLoaded變量設置成true,標識已經讀取完畢,并且調用notifyAll,讓其他線程被喚醒
而在getString
、getInt
等函數中,都會調用:
public String getString(String key, String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
首先調用awaitLoadedLocked
方法,該函數體是一個while循環,一直等待mLoaded被設置成true,否則無論當前線程是否拋出了InterruptedException,都會一直處于wait狀態。所以,如果SharedPreferences太大的話,讀取的時間會越來越長,如果在主線程調用了getString
等方法的話,會等待子線程把數據讀取完之后才會返回值,建議不要讓一個XML太大,可以分多個XML存儲,這樣不會有ANR的風險。
讀取的過程比較簡單,而寫入的過程會非常復雜,因為需要考慮多線程,多進程,什么時候寫入,同時寫入等等非常規情況的處理。
等待讀取完成后,創建出一個EditorImpl
對象,該對象中只有一個HashMap用來保存變更的Key-Value,并且可以調用clear
方法將mClear設置成true
調用apply方法
public void apply() {
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.add(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.remove(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// Okay to notify the listeners before it's hit disk
// because the listeners should always get the same
// SharedPreferences instance back, which has the
// changes reflected in memory.
notifyListeners(mcr);
}
從apply函數中可以看到:
調用commitToMemory方法
a) 創建一個MemoryCommitResult對象
b) 判斷mDiskWritesInFlight是否大于0,如果大于0的話,說明之前已經有正在往磁盤里寫的任務了,那么則將改變的值copy到一個新的Map中
c) 將map放入MemoryCommitResult,并且將mDiskWritesInFlight加1
d) 判斷當前SharedPreferences是否已經注冊過Listener,如果注冊過的話,那么將Listener放到MemoryCommitResult中,以便后續的回調使用
e) 判斷mClear是否被設置,如果被設置的話,那么就會將當前SharedPreferencesImpl中的Map清空,并將結果的changesMade設置成true,標識在內存中值已經發生改變
f) 遍歷改變的Map對象,判斷要改變的值與當前值是否相同,不同的話,則改變當前值。并將結果的changesMade設置成true,標識在內存中值已經發生改變
g) 判斷是否有Listener,如果有,則將有改變的Key添加到MemoryCommitResult中的keysModified
這個Set中,最后將EditorImpl中的Map給清空獲取到了內存改變的值之后,會創建awaitCommit以及postWriteRunnable兩個Runnable:
awaitCommit:該Runnable中執行CountDownlaunch的await方法進行等待,并且會被添加到QueueWork中
postWriteRunnable:該Runnable執行awaitCommit的run方法,并且將awaitCommit從QueueWork中移除調用enqueueDiskWrite方法,開始往寫磁盤隊列中插入。
a) 創建一個writeToDiskRunnable對象,該Runnable中完成將MemoryCommitResult寫入文件的操作
b) 判斷postWriteRunnable是否為空,該判斷主要是用來判斷當前的操作是commit還是apply,因為commit的話,是一個同步操作,需要等待寫完磁盤之后,才能繼續往下運行,而apply是個異步操作,當異步寫完磁盤之后,會調用postWriteRunnable.run方法。
c) 如果是調用commit的話,則判斷mDiskWritesInFlight是否為1,如果為1的話,那么就說明當前沒有寫磁盤任務,那么就直接調用writeToDiskRunnable.run方法,執行完之后返回,否則將寫入文件的操作放到一個單線程池中慢慢執行。
在writeToFile
中,會將每一個MemoryCommitResult都寫到文件中
- 判斷XML文件是否存在,如果存在的話,那么判斷當前內存值是否有改變,如果沒有改變的話,就調用setDiskWriteResult方法,將之前的CountDownLaunch減一,讓原來等待的線程處于就緒狀態,并且將寫入成功的標志位設置成true,標識寫入成功
- 判斷.bak文件是否存在,如果不存在的話,那么則將xml文件renameTo備份文件,如果備份失敗的話,那么則標記寫入失敗,返回。如果bak文件存在的話,那么則將原來的XML文件刪除
- 得到XML對應的FileOutputStream,如果獲取失敗的話(如無權限,創建文件失敗等等),則標記寫入失敗并且返回
- 將完整的Map對象寫入XML中,并且調用FileUtils.sync進行文件同步,同步完之后,關閉輸出流,并且設置文件權限
- 寫入成功后,刪除.bak文件,并且標記寫入文件成功,并且返回,如果寫文件過程中遇到異常,則直接將XML文件刪除,并且標記寫入失敗。
在寫完文件之后,將mDiskWritesInFlight減一,并且判斷postWriteRunnable是否存在,如果有的話,則執行postWriteRunnable。
最后沒有搞明白為什么要有個CountDownLaunch,因為CountDownLaunch.await是在最后被執行的,而那時候早就已經調用了CountDownLaunch.countdown了,所以貌似沒什么用。