本文是獨立解析源碼的第二篇,SharedPreference 是一個 Android 開發自帶的適合保存輕量級數據的 K-V 存儲庫,它使用了 XML 的方式來存儲數據,比如我就經常用它保存一些如用戶登錄信息等輕量級數據。那么今天就讓我們來分析一下它的源碼,研究一下其內部實現。
獲取SharedPreferences
我們在使用 SharedPreferences 時首先是需要獲取到這個 SharedPreferences 的,因此我們首先從 SharedPreferences 的獲取入手,來分析其源碼。
根據名稱獲取 SP
不論是在 Activity 中調用 getPreferences() 方法還是調用 Context 的 getSharedPreferences 方法,最終都是調用到了 ContextImpl 的 getSharedPreferences(String name, int mode) 方法。我們先看看它的代碼:
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
// At least one application in the world actually passes in a null
// name. This happened to work because when we generated the file name
// we would stringify it to "null.xml". Nice.
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
可以看到,它首先對 Android 4.4 以下的設備做了特殊處理,之后將對 mSharedPrefsPaths 的操作加了鎖。mSharedPrefsPaths 的聲明如下:
private ArrayMap<String, File> mSharedPrefsPaths;
可以看到它是一個以 name 為 key,name 對應的 File 為 value 的 HashMap。首先調用了 getSharedPreferencesPath 方法構建出了 name 對應的 File,將其放入 map 后再調用了 getSharedPreferences(File file, int mode) 方法。
獲取 SP 名稱對應的 File 對象
我們先看看是如何構建出 name 對應的 File 的。
@Override
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
可以看到,調用了 makeFilename 方法來創建一個名為 name.xml 的 File。makeFilename 中僅僅是做了一些判斷,之后 new 出了這個 File 對象并返回。
可以看到,SharedPreference 確實是使用 xml 來保存其中的 K-V 數據的,而具體存儲的路徑我們這里就不再關心了,有興趣的可以點進去看看。
根據創建的 File 對象獲取 SP
我們接著看到獲取到 File 并放入 Map 后調用的 getSharedPreferences(file, mode) 方法:
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked(); // 1
sp = cache.get(file);
if (sp == null) {
checkMode(mode);
if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
if (isCredentialProtectedStorage()
&& !getSystemService(UserManager.class)
.isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
throw new IllegalStateException("SharedPreferences in credential encrypted "
+ "storage are not available until after user is unlocked");
}
}
sp = new SharedPreferencesImpl(file, mode); // 2
cache.put(file, sp);
return sp;
}
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
// If somebody else (some other process) changed the prefs
// file behind our back, we reload it. This has been the
// historical (if undocumented) behavior.
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
首先可以看到注釋 1 處,這里調用了 getSharedPreferencesCacheLocked 來獲取到了一個 ArrayMap<File, SharedPreferencesImpl>,之后再從這個 Map 中嘗試獲取到對應的 SharedPreferencesImpl 實現類(簡稱 SPI)。
之后看到注釋 2 處,當獲取不到對應 SPI 時,再創建一個對應的 SPI,并將其加入這個 ArrayMap 中。
這里很明顯是一個緩存機制的實現,以加快之后獲取 SP 的速度,同時可以發現,SP 其實只是一個接口,而 SPI 才是其具體的實現類。
緩存機制
那么我們先來看看其緩存機制,進入 getSharedPreferencesCacheLocked 方法:
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
final String packageName = getPackageName();
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}
顯然,這里有個全局的 ArrayMap:sSharedPrefsCache。
它是一個 ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> 類型的 Map,而從代碼中可以看出,它是根據 PackageName 來保存不同的 SP 緩存 Map 的,通過這樣的方式,就保證了不同 PackageName 中相同 name 的 SP 從緩存中0拿到的數據是不同的。
SharedPreferencesImpl
那么終于到了我們 SPI 的創建了,在 cache 中找不到對應的 SPI 時,就會 new 出一個 SPI,看看它的構造函數:
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file); // 1
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk(); // 2
}
可以看到注釋 1 處它調用了 makeBackupFile 來進行備份文件的創建。
之后在注釋 2 處則調用了 startLoadFromDisk 來開始從 Disk 載入信息。
首先我們看看 makeBackupFile 方法:
static File makeBackupFile(File prefsFile) {
return new File(prefsFile.getPath() + ".bak");
}
很簡單,返回了一個同目錄下的后綴名為 .bak 的同名文件對象。
從 Disk 加載數據
接著,我們看看 startLoadFromDisk 方法:
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
可以看到,首先在加鎖的情況下對 mLoaded 進行了修改,之后則開了個名為「SharedPreferencesImpl-load」的線程來加載數據。
我們看到 loadFromDisk 方法:
private void loadFromDisk() {
synchronized (mLock) { // 1
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
// Debugging
if (mFile.exists() && !mFile.canRead()) {
Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
}
Map map = null;
StructStat stat = null;
try { // 2
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
map = XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
/* ignore */
}
synchronized (mLock) { // 3
mLoaded = true;
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
mLock.notifyAll();
}
}
代碼比較長,我們慢慢分析
首先在注釋 1 處,如果已經加載過,則不再進行加載,之后又開始判斷是否存在備份文件,若存在則直接將備份文件直接修改為數據文件 ${name}.xml。
之后在注釋 2 處,通過 XmlUtils 將 xml 中的數據讀取為一個 Map。由于本文主要是對 SP 的大致流程的解讀,因此關于 XML 文件的具體讀取部分,有興趣的讀者可以自己進入源碼研究。
之后在注釋 3 處,進行了一些收尾處理,將 mLoaded 置為 true,并對 mMap 進行了判空處理,以保證在 xml 沒有數據的情況下其仍不為 null,最后釋放了這個讀取的鎖,表示讀取成功。
編輯 SharedPreferences
我們都知道,真正對 SP 的操作其實都是在 Editor 中的,它其實是一個接口,具體的實現類為 EditorImpl。讓我們先看看 Editor 的獲取:
獲取 Editor
看到 SharedPreferencesImpl 的 edit 方法:
public Editor edit() {
// TODO: remove the need to call awaitLoadedLocked() when
// requesting an editor. will require some work on the
// Editor, but then we should be able to do:
//
// context.getSharedPreferences(..).edit().putString(..).apply()
//
// ... all without blocking.
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
可以看到,這里首先先調用了 awaitLoadedLocked() 方法來等待讀取的完成,當讀取完成后才會真正創建并返回 EditorImpl 對象。
等待讀取機制
由于讀取過程是一個異步的過程,很有可能導致讀取還沒結束,我們就開始編輯,因此這里用到了一個 awaitLoadedLocked 方法來阻塞線程,直到讀取過程完成,下面我們可以先看看 awaitLoadedLocked 方法:
private void awaitLoadedLocked() {
if (!mLoaded) {
// Raise an explicit StrictMode onReadFromDisk for th
// thread, since the real read will be in a different
// thread and otherwise ignored by StrictMode.
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
}
可以看到,這里會阻塞直到 mLoaded 為 true,這樣就保證了該方法后的方法都會在讀取操作進行后執行。
EditorImpl
前面我們提到了 Edit 的真正實現類是 EditorImpl,它其實是 SPI 的一個內部類。它內部維護了一個Map<String, Object>: mModified,通過 mModified 來存放對 SP 進行的操作,此時還不會提交到 SPI 中的 mMap,我們做的操作都是在改變 mModified。
下面列出一些 EditorImpl 對外提供的修改接口,其實都是在對 mModified 這個 Map 進行修改,具體代碼就不再講解,比較簡單:
public Editor putString(String key, @Nullable String val
synchronized (mLock) {
mModified.put(key, value);
return this;
}
}
public Editor putStringSet(String key, @Nullable Set<Str
synchronized (mLock) {
mModified.put(key,
(values == null) ? null : new HashSet<St
return this;
}
}
public Editor putInt(String key, int value) {
synchronized (mLock) {
mModified.put(key, value);
return this;
}
}
public Editor putLong(String key, long value) {
synchronized (mLock) {
mModified.put(key, value);
return this;
}
}
public Editor putFloat(String key, float value) {
synchronized (mLock) {
mModified.put(key, value);
return this;
}
}
public Editor putBoolean(String key, boolean value) {
synchronized (mLock) {
mModified.put(key, value);
return this;
}
}
public Editor remove(String key) {
synchronized (mLock) {
mModified.put(key, this);
return this;
}
}
public Editor clear() {
synchronized (mLock) {
mClear = true;
return this;
}
}
提交 SharedPreferences
提交本來可以放到編輯中的,但因為它才是重頭戲,因此我們單獨拎出來講一下。
我們都知道 SP 的提交有兩種方式——apply 和 commit。下面我們來分別分析:
apply
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory(); // 1
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
if (DEBUG && mcr.wasWritten) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " applied after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); // 2
// Okay to notify the listeners before it's hit disk
// because the listeners should always get the same
// SharedPreferences instance back, which has the
// changes reflected in memory.
notifyListeners(mcr);
}
首先看到注釋 1 處,調用了 commitToMemory 方法,它內部就是將原先讀取進來的 mMap 與剛剛修改過的 mModified 進行合并,并存儲于返回的 MemoryCommitResult mcr中。
而在注釋 2 處,調用了 enqueueDiskWrite 方法,傳入了之前構造的 Runnable 對象,這里的目的是進行一個異步的寫操作。
前面提到的兩個方法,我們放到后面分析。
也就是說,apply 方法會將數據先提交到內存,再開啟一個異步過程來將數據寫入硬盤。
commit
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
MemoryCommitResult mcr = commitToMemory(); // 1
SharedPreferencesImpl.this.enqueueDiskWrite( // 2
mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
} finally {
if (DEBUG) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " committed after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
看到注釋 1 處,可以發現,同樣調用了 commitToMemory 方法進行了合并。
之后看到 2 處,同樣調用了 enqueueDiskWrite 方法,不過傳入的第二個不再是 Runnable 方法。這里提一下,如果 enqueueDiskWrite 方法傳入的第二個參數為 null,則會在當前線程執行寫入操作。
也就是說,commit 方法會將數據先提交到內存,但之后則是一個同步的過程寫入硬盤。
同步數據至內存
下面我們來看看兩個方法中都調用了的 commitToMemory 的具體實現:
private MemoryCommitResult commitToMemory() {
...
synchronized (mLock) {
boolean changesMade = false;
if (mClear) {
if (!mMap.isEmpty()) {
changesMade = true;
mMap.clear();
}
mClear = false;
}
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
if (v == this || v == null) {
if (!mMap.containsKey(k)) {
continue;
}
mMap.remove(k);
} else {
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mMap.put(k, v);
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
可以看到,具體的代碼就如同我們之前所說的一樣,將 mMap 的數據與 mModified 的數據進行了整合,之后將 mModified 重新清空。最后將合并的數據放入了 MemoryCommitResult 中。
寫入數據至硬盤
我們同樣看到 apply 和 commit 都調用了的方法 enqueueDiskWrite:
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null); // 1
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run(); // 2
return;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
可以看到 1 處,若第二個 Runnable 為 null 的話,則會將 isFromSyncCommit 置為 true,也就是寫入會是一個同步的過程。之后在注釋 2 處便進行了同步寫入。否則會構造一個 Runnable 來提供給 QueueWork 進行異步寫入。
QueueWork 類內部維護了一個 single 線程池,這樣可以達到我們異步寫入的目的。
而 writeToFile 方法中其實就是又調用了之前的 XmlUtils 來進行 XML 的寫入。
總結
SharedPreferences 其實就是一個用使用 XML 進行保存的 K-V 存儲庫。
在獲取 SP 時會進行數據的加載,將 name 對應的 xml 文件以 Map 的形式讀入到內存。
而 SP 的編輯操作其實都是在 Editor 內實現,它內部維護了一個新的 Map,所有的編輯操作其實都是在操作這個 Map,只有提交時才會與之前讀取的數據進行合并。
其提交分為兩種,apply 和 commit,它們的特性如下
-
apply
- 會將數據先提交到內存,再開啟一個異步過程來將數據寫入硬盤。
- 返回值時可能寫入操作還沒有結束
- 寫入失敗時不會有任何提示
-
commit
會將數據先提交到內存,但之后則是一個同步的過程寫入硬盤。
寫入操作結束后才會返回值
寫入失敗會有提示
因此,當我們對寫入的結果不那么關心的情況下,可以使用 apply 進行異步寫入,而當我們對寫入結果十分關心且提交后有后續操作的話最好使用 commit 來進行同步寫入。