一、概述
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
的數據,都能收到監聽。