SharedPreference的讀寫原理分析
本文由嵌入式企鵝圈原創團隊成員-阿里工程師Hao分享。
一、commit和apply
apply是非同步,commit是同步,在主執行緒中使用commit可能會影響效能,因為同步IO操作的耗時可能會比較長,兩個方法都能保證value被正確的儲存到磁碟上。兩者都是Editor類的方法,它們的具體實現在EditorImpl類中,我們先大體比較一下這兩個函式:
這兩個函式一開始都呼叫了commitToMemory這個函式來得到MemoryCommitResult物件,接著又都呼叫了enqueueDiskWrite。commit和apply呼叫enqueueDiskWrite傳入的第一個引數都是MemoryCommitResult的物件mcr,第二個引數commit傳入的是null,apply傳入的是一個Runnable,從這個Runnable的名字postWriteRunnable可以猜測它會被放在一個執行緒中去執行來非同步的寫入資料。
我們先來看commitToMemory。
commitToMemory先用到了SharedPreferencesImpl的鎖,判斷mDiskWritesInFlight大於0時,就拷貝一份mMap,把它存到MemoryCommitResult類的成員mapToWriteToDisk中,然後再把mDiskWritesInFlight加1。在把mapToWriteDisk寫入到磁碟後,mDiskWritesInFlight會減1,所以mDiskWritesInFlight大於0說明之前已經有呼叫過commitToMemory了,並且還沒有把map寫入到磁碟。
這裡mMap是SharedPreferencesImpl類的成員,讀sharedPreference中的值時就是從它裡面獲取。
在寫入sharedPreference時,在commitToMemory函式中,也是把mModified中的值更新到mMap裡。這裡的mModified是EditorImpl類的成員,在呼叫putInt、putString之類的函式時,就是把key-value放在它裡面。在遍歷mModified之前要先獲得EditorImpl物件的鎖,這樣在比較mModified和mMap時就不能再執行editor的putXXX之類的方法了。
因為apply是個非同步寫入磁碟的過程,如果已經呼叫過一次commitToMemory,但還沒真正寫入磁碟,再呼叫commitToMemory時,mDiskWritesInFlight等於1,需要再拷貝一份mMap,這樣前後兩次要準備寫入磁碟的mapToWriteToDisk是兩個不同的記憶體物件,後一次呼叫commitToMemory時,在更新mMap中的值時不會影響前一次的mapToWriteToDisk的寫入磁碟。
然後我們接著來看enqueueDiskWrite的實現。commit呼叫它時第二個引數postWriteRunnable為null,所以isFromSyncCommit為true,立即執行writeToDiskRunnable。在writeToDiskRunnable的run中,先獲得mWritingToDiskLock鎖,執行writeToFile,再把mDiskWritesInFlight減1。writeToFile方法會把MemoryCommitResult中的mapToWriteToDisk寫入磁碟檔案中。
apply呼叫enqueueDiskWrite時,第二個引數傳入的是postWriteRunnable,所以不會走isFromSyncCommit的分支,在函式最後,線上程池中起一個執行緒執行writeToDiskRunnable。在writeToDiskRunnable的最後還會執行postWriteRunnable。
commit的實現中,在enqueueDiskWrite之後接著是mcr.writtenToDiskLatch.await();
writtenToDiskLatch是一個CountDownLatch,如果它的值大於0,那麼commit在await上阻塞,在writtenToDiskLatch變為0時,才能繼續往下走執行後面的notifyListeners。
writtenToDiskLatch初始值為1,在setDiskWriteResult執行完後,計數減1,await才會從阻塞中被喚醒繼續往下執行。在writeToFile中,在真正完成寫入後的地方會呼叫setDiskWriteResult。
再來看apply的實現,在另起一個執行緒把map中的資料寫入到磁碟後,postWriteRunnable會被執行,它會去執行另一個Runnable -- awaitCommit。awaitCommit中同樣執行的是mcr.writtenToDiskLatch.await();,但要注意現在是在另一個執行緒中被執行的。
可見,writtenToDiskLatch保證了無論是commit還是apply,都必須在前一個的writeToFile完成後,才能開始新一個的commit或apply操作。
二、讀sharedPreference
前面有getInt的程式碼,可以發現它的實現很簡單,直接從mMap中根據key來找value,那mMap中的資料是在什麼時候被載入的呢?我們來看SharedPreferencesImpl的建構函式。
在startLoadFromDisk中會起一個執行緒呼叫loadFromDiskLocked從磁碟上載入資料。因此,如果一個應用中有好幾個sharedPreferences時,每個sp對應的SharedPreferencesImpl都會各自起一個執行緒去把xml中的資料載入到map中,後面所有的getXXX方法實際上都是從記憶體中取資料,不會再有讀磁碟的動作了。
每個SharedPreferencesImpl的例項一直被持有著不會釋放,因為在第一次呼叫Context的getSharedPreferences後,它會被儲存在一個static的map中,在應用的生命週期中一直存在,並且磁碟上的xml檔案在載入後也會一直被儲存在記憶體中。sSharedPrefs就是存放每個SharedPreferencesImpl例項的靜態的ArrayMap。