一吕粹、概述
SharedPreferences
在開(kāi)發(fā)當(dāng)中常被用作保存一些類(lèi)似于配置項(xiàng)這類(lèi)輕量級(jí)的數(shù)據(jù)型奥,它采用鍵值對(duì)的格式,將數(shù)據(jù)存儲(chǔ)在xml
文件當(dāng)中,并保存在data/data/{應(yīng)用包名}/shared_prefs
下:
今天我們就來(lái)一起研究一下
SP
的實(shí)現(xiàn)原理堕澄。
二、SP 源碼解析
2.1 獲取 SharedPreferences 對(duì)象
在通過(guò)SP
進(jìn)行讀寫(xiě)操作時(shí)霉咨,首先需要獲得一個(gè)SharedPreferences
對(duì)象蛙紫,SharedPreferences
是一個(gè)接口,它定義了系列讀寫(xiě)的接口途戒,其實(shí)現(xiàn)類(lèi)為SharedPreferencesImpl
坑傅、在實(shí)際過(guò)程中,我們一般通過(guò)Application喷斋、Activity唁毒、Service
的下面這個(gè)方法來(lái)獲取SP
對(duì)象:
public SharedPreferences getSharedPreferences(String name, int mode)
來(lái)獲取SharedPreferences
實(shí)例,而它們最終都是調(diào)用到ContextImpl
的getSharedPreferences
方法星爪,下面是整個(gè)調(diào)用的結(jié)構(gòu):
在
ContextImpl
當(dāng)中浆西,SharedPreferences
是以一個(gè)靜態(tài)雙重ArrayMap
的結(jié)構(gòu)來(lái)保存的:
private static ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>> sSharedPrefs;
下面,我們看一下獲取SP
實(shí)例的過(guò)程:
public SharedPreferences getSharedPreferences(String name, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
if (sSharedPrefs == null) {
sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
}
//1.第一個(gè)維度是包名.
final String packageName = getPackageName();
ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
sSharedPrefs.put(packageName, packagePrefs);
}
//2.第二個(gè)維度就是調(diào)用get方法時(shí)傳入的name顽腾,并且如果已經(jīng)存在了那么直接返回
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
的構(gòu)造傳入了一個(gè)和name
相關(guān)聯(lián)的File
,它就是我們?cè)诘谝还?jié)當(dāng)中所說(shuō)的xml
文件,在構(gòu)造函數(shù)中久信,會(huì)去預(yù)先讀取這個(gè)xml
文件當(dāng)中的內(nèi)容:
SharedPreferencesImpl(File file, int mode) {
//..
startLoadFromDisk(); //讀取xml文件的內(nèi)容
}
這里啟動(dòng)了一個(gè)異步的線(xiàn)程窖杀,需要注意的是這里會(huì)將標(biāo)志位mLoad
置為false
,后面我們會(huì)談到這個(gè)標(biāo)志的作用:
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
synchronized (SharedPreferencesImpl.this) {
loadFromDiskLocked();
}
}
}.start();
}
在loadFromDiskLocked
中裙士,將xml
文件中的內(nèi)容保存到Map
當(dāng)中入客,在讀取完畢之后,喚醒之前有可能阻塞的讀寫(xiě)線(xiàn)程:
private Map<String, Object> mMap;
private void loadFromDiskLocked() {
//1.如果已經(jīng)在加載腿椎,那么返回.
if (mLoaded) {
return;
}
//...
//2.最終保存到map當(dāng)中
map = XmlUtils.readMapXml(str);
mMap = map;
//...
//3.由于讀寫(xiě)操作只有在mLoaded變量為true時(shí)才可進(jìn)行痊项,因此它們有可能阻塞在調(diào)用讀寫(xiě)操作的方法上,因此這里需要喚醒它們酥诽。
notifyAll();
}
從SP
對(duì)象的獲取過(guò)程來(lái)看鞍泉,我們可以得出下面幾個(gè)結(jié)論:
- 與某個(gè)
name
所對(duì)應(yīng)的SP
對(duì)象需要等到調(diào)用getSharedPreferences
才會(huì)被創(chuàng)建 - 對(duì)于同一進(jìn)程而言,在
Activity/Application/Service
獲取SP
對(duì)象時(shí)肮帐,如果name
相同咖驮,它們實(shí)際上獲取到的是同一個(gè)SP
對(duì)象 - 由于使用的是靜態(tài)容器來(lái)保存,因此即使
Activity/Service
銷(xiāo)毀了训枢,它之前創(chuàng)建的SP
對(duì)象也不會(huì)被釋放托修,而SP
中的數(shù)據(jù)又是用Map
來(lái)保存的,也就是說(shuō)恒界,我們只要調(diào)用了某個(gè)name
相關(guān)聯(lián)的getSharedPreferences
方法睦刃,那么和該name
對(duì)應(yīng)的xml
文件中的數(shù)據(jù)都會(huì)被讀到內(nèi)存當(dāng)中,并且一直到進(jìn)程被結(jié)束十酣。
2.2 通過(guò) SharedPreferences 進(jìn)行讀取操作
讀取的操作很簡(jiǎn)單涩拙,它其實(shí)就是從之間預(yù)先讀取的mMap
當(dāng)中去取出對(duì)應(yīng)的數(shù)據(jù),以getBoolean
為例:
public boolean getBoolean(String key, boolean defValue) {
synchronized (this) {
awaitLoadedLocked();
Boolean v = (Boolean)mMap.get(key);
return v != null ? v : defValue;
}
}
這里唯一需要關(guān)心的是awaitLoadedLocked
方法:
private void awaitLoadedLocked() {
//這里如果判斷沒(méi)有加載完畢耸采,那么會(huì)進(jìn)入無(wú)限等待狀態(tài)
while (!mLoaded) {
try {
wait();
} catch (InterruptedException unused) {}
}
}
在這個(gè)方法中兴泥,會(huì)去檢查mLoaded
標(biāo)志位是否為true
,如果不為true
虾宇,那么說(shuō)明沒(méi)有加載完畢搓彻,該線(xiàn)程會(huì)釋放它所持有的鎖,進(jìn)入等待狀態(tài)嘱朽,直到loadFromDiskLocked
加載完xml
文件中的內(nèi)容調(diào)用notifyAll()
后旭贬,該線(xiàn)程才被喚醒。
從讀取操作來(lái)看搪泳,我們可以得出以下兩個(gè)結(jié)論:
- 任何時(shí)刻讀取操作稀轨,讀取的都是內(nèi)存中的值,而并不是
xml
文件的值森书。 - 在調(diào)用讀取方法時(shí)靶端,如果構(gòu)造函數(shù)中的預(yù)讀取線(xiàn)程沒(méi)有執(zhí)行完畢谎势,那么將會(huì)導(dǎo)致讀取的線(xiàn)程進(jìn)入等待狀態(tài)。
2.3 通過(guò) SharedPreferences 進(jìn)行寫(xiě)入操作
2.3.1 獲取 EditorImpl
當(dāng)我們需要通過(guò)SharedPreferences
寫(xiě)入信息時(shí)杨名,那么首先需要通過(guò).edit()
獲得一個(gè)Editor
對(duì)象脏榆,這里和讀取操作類(lèi)似,都是需要等到預(yù)加載的線(xiàn)程執(zhí)行完畢:
public Editor edit() {
synchronized (this) {
awaitLoadedLocked();
}
return new EditorImpl();
}
Editor
的實(shí)現(xiàn)類(lèi)為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;
}
}
}
由上面的代碼可以看出须喂,當(dāng)我們調(diào)用Editor
的putXXX
方法時(shí),實(shí)際上并沒(méi)有保存到SP
的mMap
當(dāng)中趁蕊,而僅僅是保存到通過(guò).edit()
返回的EditorImpl
的臨時(shí)變量當(dāng)中坞生。
2.3.2 apply 和 commit 方法
我們通過(guò)editor
寫(xiě)入的數(shù)據(jù),最終需要等到調(diào)用editor
的apply
和commit
方法掷伙,才會(huì)寫(xiě)入到內(nèi)存和xml
這兩個(gè)地方掸茅。
(a) apply
下面汛兜,我們先看比較常用的apply
方法:
public void apply() {
//1.將修改操作提交到內(nèi)存當(dāng)中.
final MemoryCommitResult mcr = commitToMemory();
//2.寫(xiě)入文件當(dāng)中
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); //postWriteRunnable在寫(xiě)入文件完成后進(jìn)行一些收尾操作.
//3.只要寫(xiě)入到內(nèi)存當(dāng)中汁尺,就通知監(jiān)聽(tīng)者.
notifyListeners(mcr);
}
整個(gè)apply
分為三個(gè)步驟:
- 通過(guò)
commitToMemory
寫(xiě)入到內(nèi)存中 - 通過(guò)
enqueueDiskWrite
寫(xiě)入到磁盤(pán)中 - 通知監(jiān)聽(tīng)者
其中第一個(gè)步驟很好理解吨灭,就是根據(jù)editor
中的內(nèi)容,確定哪些是需要更新的數(shù)據(jù)宙地,然后把SP
當(dāng)中的mMap
變量進(jìn)行更新摔认,之后將變化的內(nèi)容封裝成MemoryCommitResult
結(jié)構(gòu)體。
我們主要看一下第二步宅粥,是如何寫(xiě)入磁盤(pán)當(dāng)中的:
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//1.寫(xiě)入磁盤(pán)任務(wù)的runnable.
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
//1.1 寫(xiě)入磁盤(pán)
synchronized (mWritingToDiskLock) {
writeToFile(mcr);
}
//....執(zhí)行收尾操作.
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
//2.這里如果是通過(guò)apply方法調(diào)用過(guò)來(lái)的参袱,那么為false
final boolean isFromSyncCommit = (postWriteRunnable == null);
if (isFromSyncCommit) { //apply 方法不走這里
//...
writeToDiskRunnable.run();
return;
}
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
可以看出,如果調(diào)用apply
方法秽梅,那么對(duì)于xml
文件的寫(xiě)入是在異步線(xiàn)程當(dāng)中進(jìn)行的抹蚀。
(b) commit
如果調(diào)用的commit
方法,那么執(zhí)行的是如下操作:
public boolean commit() {
//1.寫(xiě)入內(nèi)存
MemoryCommitResult mcr = commitToMemory();
//2.寫(xiě)入文件
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null); //由于是同步進(jìn)行风纠,所以把收尾操作放到Runnable當(dāng)中.
//在這里執(zhí)行收尾操作..
//3.通知監(jiān)聽(tīng)
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
當(dāng)使用commit
方法時(shí)况鸣,和apply
類(lèi)似牢贸,都是三步操作竹观,只不過(guò)第二步在寫(xiě)入文件的時(shí)候,傳入的Runnable
為null
潜索,因此臭增,對(duì)于寫(xiě)入文件的操作是同步的,因此竹习,如果我們?cè)谥骶€(xiàn)程當(dāng)中調(diào)用了commit
方法誊抛,那么實(shí)際上是在主線(xiàn)程進(jìn)行IO
操作。
(c) 回調(diào)時(shí)機(jī)
- 對(duì)于
apply
方法整陌,由于它對(duì)于文件的寫(xiě)入是異步的拗窃,但是notifyListener
方法不會(huì)等到真正寫(xiě)入完成時(shí)才通知監(jiān)聽(tīng)者瞎领,因此監(jiān)聽(tīng)者在收到回調(diào)或者apply
返回時(shí),對(duì)于SP
數(shù)據(jù)的改變只是寫(xiě)入到了內(nèi)存當(dāng)中随夸,并沒(méi)有寫(xiě)入到文件當(dāng)中九默。 - 對(duì)于
commit
方法,由于它對(duì)于文件的寫(xiě)入是同步的宾毒,因此可以保證監(jiān)聽(tīng)者收到回調(diào)時(shí)或者commit
方法返回后驼修,改變已經(jīng)被寫(xiě)入到了文件當(dāng)中。
2.4 監(jiān)聽(tīng) SP 的變化
如果希望監(jiān)聽(tīng)SP
的變化诈铛,那么可以通過(guò)下面的這兩個(gè)方法:
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
synchronized(this) {
mListeners.put(listener, mContent);
}
}
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
synchronized(this) {
mListeners.remove(listener);
}
}
由于對(duì)應(yīng)于Name
的SP
在進(jìn)程中是實(shí)際上是一個(gè)單例模式乙各,因此,我們可以做到在進(jìn)程中的任何地方改變SP
的數(shù)據(jù)幢竹,都能收到監(jiān)聽(tīng)耳峦。