SharedPreferences
在開發當中常被用做保存一些相似於配置項這類輕量級的數據,它採用鍵值對的格式,將數據存儲在xml
文件當中,並保存在data/data/{應用包名}/shared_prefs
下: bash
SP
的實現原理。
在經過SP
進行讀寫操做時,首先須要得到一個SharedPreferences
對象,SharedPreferences
是一個接口,它定義了系列讀寫的接口,其實現類爲SharedPreferencesImpl
、在實際過程當中,咱們通常經過Application、Activity、Service
的下面這個方法來獲取SP
對象:app
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
文件當中的內容:post
SharedPreferencesImpl(File file, int mode) {
//..
startLoadFromDisk(); //讀取xml文件的內容
}
複製代碼
這裏啓動了一個異步的線程,須要注意的是這裏會將標誌位mLoad
置爲false
,後面咱們會談到這個標誌的做用:ui
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
synchronized (SharedPreferencesImpl.this) {
loadFromDiskLocked();
}
}
}.start();
}
複製代碼
在loadFromDiskLocked
中,將xml
文件中的內容保存到Map
當中,在讀取完畢以後,喚醒以前有可能阻塞的讀寫線程:this
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
對象的獲取過程來看,咱們能夠得出下面幾個結論:spa
name
所對應的SP
對象須要等到調用getSharedPreferences
纔會被建立Activity/Application/Service
獲取SP
對象時,若是name
相同,它們實際上獲取到的是同一個SP
對象Activity/Service
銷燬了,它以前建立的SP
對象也不會被釋放,而SP
中的數據又是用Map
來保存的,也就是說,咱們只要調用了某個name
相關聯的getSharedPreferences
方法,那麼和該name
對應的xml
文件中的數據都會被讀到內存當中,而且一直到進程被結束。讀取的操做很簡單,它其實就是從之間預先讀取的mMap
當中去取出對應的數據,以getBoolean
爲例:線程
public boolean getBoolean(String key, boolean defValue) {
synchronized (this) {
awaitLoadedLocked();
Boolean v = (Boolean)mMap.get(key);
return v != null ? v : defValue;
}
}
複製代碼
這裏惟一須要關心的是awaitLoadedLocked
方法:code
private void awaitLoadedLocked() {
//這裏若是判斷沒有加載完畢,那麼會進入無限等待狀態
while (!mLoaded) {
try {
wait();
} catch (InterruptedException unused) {}
}
}
複製代碼
在這個方法中,會去檢查mLoaded
標誌位是否爲true
,若是不爲true
,那麼說明沒有加載完畢,該線程會釋放它所持有的鎖,進入等待狀態,直到loadFromDiskLocked
加載完xml
文件中的內容調用notifyAll()
後,該線程才被喚醒。
從讀取操做來看,咱們能夠得出如下兩個結論:
xml
文件的值。當咱們須要經過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
的臨時變量當中。
咱們經過editor
寫入的數據,最終須要等到調用editor
的apply
和commit
方法,纔會寫入到內存和xml
這兩個地方。
下面,咱們先看比較經常使用的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
文件的寫入是在異步線程當中進行的。
若是調用的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
操做。
apply
方法,因爲它對於文件的寫入是異步的,可是notifyListener
方法不會等到真正寫入完成時才通知監聽者,所以監聽者在收到回調或者apply
返回時,對於SP
數據的改變只是寫入到了內存當中,並無寫入到文件當中。commit
方法,因爲它對於文件的寫入是同步的,所以能夠保證監聽者收到回調時或者commit
方法返回後,改變已經被寫入到了文件當中。若是但願監聽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
的數據,都能收到監聽。