一、概述
SharedPreferences
在開發當中常被用作儲存一些類似於配置項這類輕量級的資料,它採用鍵值對的格式,將資料儲存在xml
檔案當中,並儲存在data/data/{應用包名}/shared_prefs
下:
SP
的實現原理。
二、SP 原始碼解析
2.1 獲取 SharedPreferences 物件
在通過SP
進行讀寫操作時,首先需要獲得一個SharedPreferences
物件,SharedPreferences
是一個介面,它定義了系列讀寫的介面,其實現類為SharedPreferencesImpl
、在實際過程中,我們一般通過Application、Activity、Service
的下面這個方法來獲取SP
物件:
public SharedPreferences getSharedPreferences(String name, int mode)
複製程式碼
來獲取SharedPreferences
例項,而它們最終都是呼叫到ContextImpl
的getSharedPreferences
方法,下面是整個呼叫的結構:
ContextImpl
當中,SharedPreferences
是以一個靜態雙重ArrayMap
的結構來儲存的:
private static ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>> sSharedPrefs;
複製程式碼
下面,我們看一下獲取SP
例項的過程:
public SharedPreferences getSharedPreferences(String name, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
if (sSharedPrefs == null) {
sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
}
//1.第一個維度是包名.
final String packageName = getPackageName();
ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
sSharedPrefs.put(packageName, packagePrefs);
}
//2.第二個維度就是呼叫get方法時傳入的name,並且如果已經存在了那麼直接返回
sp = packagePrefs.get(name);
if (sp == null) {
File prefsFile = getSharedPrefsFile(name);
sp = new SharedPreferencesImpl(prefsFile, mode);
packagePrefs.put(name, sp);
return sp;
}
}
return sp;
}
複製程式碼
在上面,我們看到SharedPreferencesImpl
的構造傳入了一個和name
相關聯的File
,它就是我們在第一節當中所說的xml
檔案,在建構函式中,會去預先讀取這個xml
檔案當中的內容:
SharedPreferencesImpl(File file, int mode) {
//..
startLoadFromDisk(); //讀取xml檔案的內容
}
複製程式碼
這裡啟動了一個非同步的執行緒,需要注意的是這裡會將標誌位mLoad
置為false
,後面我們會談到這個標誌的作用:
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
synchronized (SharedPreferencesImpl.this) {
loadFromDiskLocked();
}
}
}.start();
}
複製程式碼
在loadFromDiskLocked
中,將xml
檔案中的內容儲存到Map
當中,在讀取完畢之後,喚醒之前有可能阻塞的讀寫執行緒:
private Map<String, Object> mMap;
private void loadFromDiskLocked() {
//1.如果已經在載入,那麼返回.
if (mLoaded) {
return;
}
//...
//2.最終儲存到map當中
map = XmlUtils.readMapXml(str);
mMap = map;
//...
//3.由於讀寫操作只有在mLoaded變數為true時才可進行,因此它們有可能阻塞在呼叫讀寫操作的方法上,因此這裡需要喚醒它們。
notifyAll();
}
複製程式碼
從SP
物件的獲取過程來看,我們可以得出下面幾個結論:
- 與某個
name
所對應的SP
物件需要等到呼叫getSharedPreferences
才會被建立 - 對於同一程式而言,在
Activity/Application/Service
獲取SP
物件時,如果name
相同,它們實際上獲取到的是同一個SP
物件 - 由於使用的是靜態容器來儲存,因此即使
Activity/Service
銷燬了,它之前建立的SP
物件也不會被釋放,而SP
中的資料又是用Map
來儲存的,也就是說,我們只要呼叫了某個name
相關聯的getSharedPreferences
方法,那麼和該name
對應的xml
檔案中的資料都會被讀到記憶體當中,並且一直到程式被結束。
2.2 通過 SharedPreferences 進行讀取操作
讀取的操作很簡單,它其實就是從之間預先讀取的mMap
當中去取出對應的資料,以getBoolean
為例:
public boolean getBoolean(String key, boolean defValue) {
synchronized (this) {
awaitLoadedLocked();
Boolean v = (Boolean)mMap.get(key);
return v != null ? v : defValue;
}
}
複製程式碼
這裡唯一需要關心的是awaitLoadedLocked
方法:
private void awaitLoadedLocked() {
//這裡如果判斷沒有載入完畢,那麼會進入無限等待狀態
while (!mLoaded) {
try {
wait();
} catch (InterruptedException unused) {}
}
}
複製程式碼
在這個方法中,會去檢查mLoaded
標誌位是否為true
,如果不為true
,那麼說明沒有載入完畢,該執行緒會釋放它所持有的鎖,進入等待狀態,直到loadFromDiskLocked
載入完xml
檔案中的內容呼叫notifyAll()
後,該執行緒才被喚醒。
從讀取操作來看,我們可以得出以下兩個結論:
- 任何時刻讀取操作,讀取的都是記憶體中的值,而並不是
xml
檔案的值。 - 在呼叫讀取方法時,如果建構函式中的預讀取執行緒沒有執行完畢,那麼將會導致讀取的執行緒進入等待狀態。
2.3 通過 SharedPreferences 進行寫入操作
2.3.1 獲取 EditorImpl
當我們需要通過SharedPreferences
寫入資訊時,那麼首先需要通過.edit()
獲得一個Editor
物件,這裡和讀取操作類似,都是需要等到預載入的執行緒執行完畢:
public Editor edit() {
synchronized (this) {
awaitLoadedLocked();
}
return new EditorImpl();
}
複製程式碼
Editor
的實現類為EditorImpl
,以putString
為例:
public final class EditorImpl implements Editor {
private final Map<String, Object> mModified = Maps.newHashMap();
private boolean mClear = false;
public Editor putString(String key, @Nullable String value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
}
複製程式碼
由上面的程式碼可以看出,當我們呼叫Editor
的putXXX
方法時,實際上並沒有儲存到SP
的mMap
當中,而僅僅是儲存到通過.edit()
返回的EditorImpl
的臨時變數當中。
2.3.2 apply 和 commit 方法
我們通過editor
寫入的資料,最終需要等到呼叫editor
的apply
和commit
方法,才會寫入到記憶體和xml
這兩個地方。
(a) apply
下面,我們先看比較常用的apply
方法:
public void apply() {
//1.將修改操作提交到記憶體當中.
final MemoryCommitResult mcr = commitToMemory();
//2.寫入檔案當中
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); //postWriteRunnable在寫入檔案完成後進行一些收尾操作.
//3.只要寫入到記憶體當中,就通知監聽者.
notifyListeners(mcr);
}
複製程式碼
整個apply
分為三個步驟:
- 通過
commitToMemory
寫入到記憶體中 - 通過
enqueueDiskWrite
寫入到磁碟中 - 通知監聽者
其中第一個步驟很好理解,就是根據editor
中的內容,確定哪些是需要更新的資料,然後把SP
當中的mMap
變數進行更新,之後將變化的內容封裝成MemoryCommitResult
結構體。
我們主要看一下第二步,是如何寫入磁碟當中的:
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//1.寫入磁碟任務的runnable.
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
//1.1 寫入磁碟
synchronized (mWritingToDiskLock) {
writeToFile(mcr);
}
//....執行收尾操作.
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
//2.這裡如果是通過apply方法呼叫過來的,那麼為false
final boolean isFromSyncCommit = (postWriteRunnable == null);
if (isFromSyncCommit) { //apply 方法不走這裡
//...
writeToDiskRunnable.run();
return;
}
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
複製程式碼
可以看出,如果呼叫apply
方法,那麼對於xml
檔案的寫入是在非同步執行緒當中進行的。
(b) commit
如果呼叫的commit
方法,那麼執行的是如下操作:
public boolean commit() {
//1.寫入記憶體
MemoryCommitResult mcr = commitToMemory();
//2.寫入檔案
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null); //由於是同步進行,所以把收尾操作放到Runnable當中.
//在這裡執行收尾操作..
//3.通知監聽
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
複製程式碼
當使用commit
方法時,和apply
類似,都是三步操作,只不過第二步在寫入檔案的時候,傳入的Runnable
為null
,因此,對於寫入檔案的操作是同步的,因此,如果我們在主執行緒當中呼叫了commit
方法,那麼實際上是在主執行緒進行IO
操作。
(c) 回撥時機
- 對於
apply
方法,由於它對於檔案的寫入是非同步的,但是notifyListener
方法不會等到真正寫入完成時才通知監聽者,因此監聽者在收到回撥或者apply
返回時,對於SP
資料的改變只是寫入到了記憶體當中,並沒有寫入到檔案當中。 - 對於
commit
方法,由於它對於檔案的寫入是同步的,因此可以保證監聽者收到回撥時或者commit
方法返回後,改變已經被寫入到了檔案當中。
2.4 監聽 SP 的變化
如果希望監聽SP
的變化,那麼可以通過下面的這兩個方法:
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
synchronized(this) {
mListeners.put(listener, mContent);
}
}
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
synchronized(this) {
mListeners.remove(listener);
}
}
複製程式碼
由於對應於Name
的SP
在程式中是實際上是一個單例模式,因此,我們可以做到在程式中的任何地方改變SP
的資料,都能收到監聽。