SharedPreferences作為Android常用的持久化元件,很常用,在系統設定應用中就大量地被使用。相信大家基本都是用過,但是在使用過程中,大家是否又知道它的實作原理呢?
基本使用
SharedPreferences使用很簡單,比如我們要它儲存某個字元串,在Activity使用如下:
/**
* save number
* @param num
*/
private void saveNumber(String num) {
SharedPreferences sharedPreferences = getSharedPreferences("light_persist", MODE_PRIVATE);
sharedPreferences.edit().putString("number", num).apply();
//sharedPreferences.edit().putString("number", number).commit();
}
上面代碼執行後,會将變量num儲存在/data/data/應用包名/shared_prefs/light_persist.xml。SharedPreferences使用很簡單,但這并不表示它本身也簡單,下面從三個角度來深入了解SharedPreferences的實作原理
對象初始化
在上面的例子中,我們是在Activity通過調用getSharedPreferences()擷取SharedPreferences對象,我們看下它的具體實作是怎樣的。首先,我們要明白以下幾點:
- Activity繼承了ContextThemeWrapper,ContextThemeWrapper繼承了ContextWrapper,而ContextWrapper繼承了抽象類Context,getSharedPreferences()為Context中的抽象方法;ContextWrapper重寫了getSharedPreferences()方法,是以Activity中調用getSharedPreferences()方法,實際上調用的是ContextWrapper裡面的方法。
- ContextWrapper中getSharedPreferences()方法的實作是調用了Context對象mBase的getSharedPreferences()。mBase具體對象為ContextImpl,也就是說Context.getSharedPreferences()具體實作在ContextImpl中。
對于第一點,通過類的繼承關系很容易知道;對于第二點,這個會涉及到Activity的啟動過程,在ActivityThread的performLaunchActivity()方法中會初始化context,context初始化的時序圖如下
Activity在啟動的時候會建立ContextImpl對象,并調用Activity的attach()方法将ContextImpl對象傳遞下去,最終給ContextWrapper的Context對象mBase指派。
在明白Activity中調用getSharedPreferences()方法實際上調用了CcontextImpl.getSharedPreferences()後,我們就很容易明白SharedPreferences對象是如何初始化的了。SharedPreferences對象初始化過程如下所示
下面我們就看下SharedPreference初始化的代碼實作
@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);
//從緩存集合中取name對應的file,如果集合中沒有,則根據name建立對應的file并添加進集合
if (file == null) {
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
//getSharedPreferencesCacheLocked()根據應用包名從緩存集合中取ArrayMap集合
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
//mode校驗,在Android7.0開始不再支援MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE,
//也就是不支援其他應用讀寫該應用的SharedPreference檔案。
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");
}
}
//建立SharedPreferences接口實作類對象SharedPreferencesImpl,并存儲在集合中。
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
//如果mode設定為了MODE_MULTI_PROCESS且targetSdkVersion小于Android3.0,則從硬碟中加載SharedPreferences檔案
//MODE_MULTI_PROCESS是對應用多程序的一種支援,但是在Android3.0版本開始已經棄用了
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;
}
至此SharedPreferencesImpl對象就建立完成了,至于SharedPreferencesImpl的構造方法,具體就不展開,它主要作用是變量初始化、建立備份檔案和從硬碟中加載SharedPredference檔案。最後我們看下SharedPreferences接口說明。
* Interface for accessing and modifying preference data returned by {@link
* Context#getSharedPreferences}. For any particular set of preferences,
* there is a single instance of this class that all clients share.
* Modifications to the preferences must go through an {@link Editor} object
* to ensure the preference values remain in a consistent state and control
* when they are committed to storage. Objects that are returned from the
* various <code>get</code> methods must be treated as immutable by the application.
*
* <p><em>Note: This class does not support use across multiple processes.</em>
有兩點需要注意:一,對于任何特定的一組preferences,所有用戶端都共享一個此接口實作類(也就是SharedPreferencesImpl)的單個執行個體,這個一點可以在ContextImpl.getSharedPreferencesCacheLocked()方法中得到驗證,它是根據應用包名從緩存集合中取ArrayMap集合。二,不支援跨程序使用,這個在Android3.0已經棄用了。
寫入實作
往SharedPreferences中儲存資料,我們是通過Editor接口實作的,通過調用Editor.putXxx().apply()或Editor.putXxx().commit()方法将資料儲存起來。我們先看下涉及到的類關系圖
Editor是SharedPreferences接口的嵌套接口,它的實作類是SharedPreferencesImpl的嵌套類EditorImpl,是以要看資料是怎麼寫入的,我們就需要分析EditorImpl裡面的putXxx() 和apply()或者commit()方法,大多數同學應該知道commit()和apply()方法的差別,前者是同步方法且運作在主線程中,後者是異步方法運作在子線程中,我們先看下apply()的實作方式,了解apply()之後,commit()也就不再話下。
首先建立Editor接口實作類EditorImpl執行個體對象,然後調用EditorImpl.putString(),最後通過apply()送出一個寫入資料的異步任務。我們重點關注下apply()方法實作,它的實作分為以下幾步:
- 建立MemoryCommitResult對象,該對象中存儲了寫入SharedPreferences的資料。
- 建立阻塞CountDownLatch(MemoryCommitReuslt.writtenToDiskLatch)的任務,調用QueueWork.addFinisher()方法緩存,緩存的任務不一定會在QueueWork執行。唯一可能會執行的情況是QueueWork.waitToFinish()方法中調用。而waitToFinish()方法将會在Activity的onPause()之後,BroadcastReceiver的onReceive之後,Service的stop的時候調用,具體調用的地方是在ActivityThread中,感興趣的同學可查閱AcaptivityThread。
- 将寫入硬碟的操作的任務入QueueWork隊列,由QueueWork中的HandlerThread執行寫入的任務。
- 通知監聽
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
//這裡作用是如果寫preference到硬碟沒有結束,就阻塞QueueWork中執行寫preference的線程,直到寫入完成。如果preference已經寫入硬碟,寫入過程中會調用mcr.writtenToDiskLatch.countDown()方法,而此時下面的await()方法不會阻塞,因為writtenToDiskLatch初始化的count為1.
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
if (DEBUG && mcr.wasWritten) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " applied after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
};
//addFinisher方法的實作是将Runnable對象加入到一個名為sFinisher的集合,Finisher可以了解為執行最後操作的管理者。
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
//将等待任務從Finisher中移除,因為此Runnable是在寫入操作完成之後調用,是以就需要從 QueueWork.sFinisher集合中移除;
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// 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);
}
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
//将preference寫入磁盤,并調用mcr.writtenToDiskLatch.countDown()釋放latch
//writtenToDiskLatch初始化的latch個數為1;
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();
return;
}
}
//QueueWork.queue()最終實作是通過Queue中的HandlerThread執行writeToDiskRunnable任務。
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
以上就是通過apply()方式寫入preference的實作方式,相關說明已在代碼中注釋。補充一點,CountDownLatch是一種同步機制,作用類似于它的字面意思,可以了解它是一道門,門上有一個或者多個門闩,要想通過這道門隻有所有門闩都打開後通過這道門,否則隻能一直等待。
有一點需要注意,當我們通過Editor改變某個SharedPreferences檔案的一項preference時,實際上最終會将該檔案的所有preference重新寫入檔案,這一點可以從commitToMemory()方法中看出,是以建議不要用SharedPreferences存儲資料量大的内容;另外頻繁改變的資料應和其他資料放在不同的SharedPreferences檔案中。
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
// We optimistically don't make a deep copy until
// a memory commit comes in when we're already
// writing to disk.
//目前寫入磁盤的操作大于0,則先拷貝
if (mDiskWritesInFlight > 0) {
// We can't modify our mMap as a currently
// in-flight write owns it. Clone it before
// modifying it.
// noinspection unchecked
mMap = new HashMap<String, Object>(mMap);
}
mapToWriteToDisk = mMap;//mMap就是所有的preference
mDiskWritesInFlight++;//此變量會在真正寫入磁盤的操作完成後遞減
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList<String>();
listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (mEditorLock) {
boolean changesMade = false;
//是否調用了Edit.clear()方法
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
mClear = false;
}
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
// "this" is the magic value for a removal mutation. In addition,
// setting a value to "null" for a given key is specified to be
// equivalent to calling remove on that key.
//如果value為EditorImpl對象本身或者為null,則将它從map中移除
if (v == this || v == null) {
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k);
} else {
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mapToWriteToDisk.put(k, v);
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
至于Editor.commit()方法就不展開了,commit實作比apply()更簡單些。
讀取實作
在SharedPrefrencesImpl的構造方法中會加載儲存的preference,并存入集合mMap。當我們通過SharedPreferences.getXxx()方法是,就是從mMap中取出對應的值。
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
//判斷SharedPreferences檔案是否加載,如果沒加載則阻塞知道檔案加載完成
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
總結
- SharedPreferences是接口,它的實作類是SharedPreferencesImpl。在Activity中調用getSharedPreferences()最終調用的是ContextImpl.getSharedPreferences()方法;
- Eidtor.apply()将preference寫入磁盤的任務是在QueueWork中的HandlerThread線程中執行;而Eidtor.commit()将preference寫入磁盤的任務是運作在主線程中。兩者寫入的過程中都會利用CountDownLatch同步機制來保證寫入任務執行後才執行下一個任務。