[轉(zhuǎn)]一文讀懂 SharedPreferences 的缺陷及一點點思考
SharedPreferences 是系統(tǒng)提供的一個適合用于存儲少量鍵值對數(shù)據(jù)的持久化存儲方案,結(jié)構(gòu)簡單柄沮,使用方便闭树,很多應(yīng)用都會使用到檐什。另一方面旺嬉,SharedPreferences 存在的問題也挺多的判哥,當(dāng)中 ANR 問題就屢見不鮮潭兽,字節(jié)跳動技術(shù)團隊就曾經(jīng)發(fā)布過一篇文章專門來闡述該問題:剖析 SharedPreference apply 引起的 ANR 問題桅咆。到了現(xiàn)在,Google Jetpack 也推出了一套新的持久化存儲方案:DataStore拥知,大有取代 SharedPreferences 的趨勢
本文就結(jié)合源碼來剖析 SharedPreferences 存在的缺陷以及背后的具體原因踏拜,基于 SDK 30 進行分析,讓讀者做到知其然也知其所以然低剔,并在最后介紹下我個人的一種存儲機制設(shè)計方案速梗,希望對你有所幫助.
不得不說的坑
會一直占用內(nèi)存
SharedPreferences 本身是一個接口肮塞,具體的實現(xiàn)類是 SharedPreferencesImpl,Context 中各個和 SP 相關(guān)的方法都是由 ContextImpl 來實現(xiàn)的姻锁。我們項目中的每個 SP 或多或少都是保存著一些鍵值對枕赵,而每當(dāng)我們獲取到一個 SP 對象時,其對應(yīng)的數(shù)據(jù)就會一直被保留在內(nèi)存中位隶,直到應(yīng)用進程被終結(jié)拷窜,因為每個 SP 對象都被系統(tǒng)作為靜態(tài)變量緩存起來了,對應(yīng) ContextImpl 中的靜態(tài)變量 sSharedPrefsCache
class ContextImpl extends Context {
//先根據(jù)應(yīng)用包名緩存所有 SharedPreferences
//再根據(jù) xmlFile 和具體的 SharedPreferencesImpl 對應(yīng)上
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
//根據(jù) fileName 拿到對應(yīng)的 xmlFile
private ArrayMap<String, File> mSharedPrefsPaths;
}
每個 SP 都對應(yīng)一個本地磁盤中的 xmlFile涧黄,fileName 則是由開發(fā)者來顯式指定的篮昧,每個 xmlFile 都對應(yīng)一個 SharedPreferencesImpl。所以 ContextImpl 的邏輯是先根據(jù) fileName 拿到 xmlFile笋妥,再根據(jù) xmlFile 拿到 SharedPreferencesImpl懊昨,最終應(yīng)用內(nèi)所有的 SharedPreferencesImpl 就都會被緩存在 sSharedPrefsCache
這個靜態(tài)變量中。此外春宣,由于 SharedPreferencesImpl 在初始化后就會自動去加載 xmlFile 中的所有鍵值對數(shù)據(jù)酵颁,而 ContextImpl 內(nèi)部并沒有看到有清理 sSharedPrefsCache
緩存的邏輯,所以 sSharedPrefsCache
會被一直保留在內(nèi)存中直到進程終結(jié)月帝,其內(nèi)存大小會隨著我們引用到的 SP 增多而加大躏惋,這就可能會持續(xù)占用很大一塊內(nèi)存空間
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
···
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);
}
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
···
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
···
return sp;
}
@GuardedBy("ContextImpl.class")
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;
}
GetValue 可能導(dǎo)致線程阻塞
SharedPreferencesImpl 在構(gòu)造函數(shù)中直接就啟動了一個子線程去加載磁盤文件,這意味著該操作是一個異步操作(我好像在說廢話)嚷辅,如果文件很大或者線程調(diào)度系統(tǒng)沒有馬上啟動該線程的話其掂,那么該操作就需要一小段時間后才能執(zhí)行完畢
final class SharedPreferencesImpl implements SharedPreferences {
@UnsupportedAppUsage
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
@UnsupportedAppUsage
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
//加載磁盤文件
loadFromDisk();
}
}.start();
}
}
而如果我們在初始化 SharedPreferencesImpl 后緊接著就去 getValue 的話,勢必也需要確保子線程已經(jīng)加載完成后才去進行取值操作潦蝇,所以 SharedPreferencesImpl 就通過在每個 getValue 方法中調(diào)用 awaitLoadedLocked()
方法來判斷是否需要阻塞外部線程,確保取值操作一定會在子線程執(zhí)行完畢后才執(zhí)行深寥。loadFromDisk()
方法會在任務(wù)執(zhí)行完畢后調(diào)用 mLock.notifyAll()
喚醒所有被阻塞的線程
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
//判斷是否需要讓外部線程等待
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
@GuardedBy("mLock")
private void awaitLoadedLocked() {
if (!mLoaded) {
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
//還未加載線程攘乒,讓外部線程暫停等待
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
private void loadFromDisk() {
···
synchronized (mLock) {
mLoaded = true;
mThrowable = thrown;
try {
if (thrown == null) {
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
}
} catch (Throwable t) {
mThrowable = t;
} finally {
//喚醒所有被阻塞的線程
mLock.notifyAll();
}
}
}
所以說,如果 SP 存儲的數(shù)據(jù)量很大的話惋鹅,那么就有可能導(dǎo)致外部的調(diào)用者線程被阻塞则酝,嚴重時甚至可能導(dǎo)致 ANR。當(dāng)然闰集,這種可能性也只是發(fā)生在加載磁盤文件完成之前沽讹,當(dāng)加載完成后 awaitLoadedLocked()
方法自然不會阻塞線程
GetValue 不保證數(shù)據(jù)類型安全
以下代碼在編譯階段是完全正常的,但在運行時就會拋出異常:java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
武鲁。很明顯爽雄,這是由于同個 key 先后對應(yīng)了不同數(shù)據(jù)類型導(dǎo)致的,SharedPreferences 沒有辦法對這種操作做出限制沐鼠,完全需要依賴于開發(fā)者自己的代碼規(guī)范來進行限制
val sharedPreferences: SharedPreferences = getSharedPreferences("UserInfo", Context.MODE_PRIVATE)
val key = "userName"
val edit = sharedPreferences.edit()
edit.putInt(key, 11)
edit.apply()
val name = sharedPreferences.getString(key, "")
不支持多進程數(shù)據(jù)共享
在獲取 SP 實例的時候需要傳入一個 int 類型的 mode 標記位參數(shù)挚瘟,存在一個和多進程相關(guān)的標記位 MODE_MULTI_PROCESS叹谁,該標記位能起到一定程度的多進程數(shù)據(jù)同步的保障,但作用不大乘盖,且并不保證多進程并發(fā)安全性
val sharedPreferences: SharedPreferences = getSharedPreferences("UserInfo", Context.MODE_MULTI_PROCESS)
上文有講到焰檩,SharedPreferencesImpl 在被加載后就會一直保留在內(nèi)存中,之后每次獲取都是直接使用緩存數(shù)據(jù)订框,通常情況下也不會再次去加載磁盤文件析苫。而 MODE_MULTI_PROCESS 起到的作用就是每當(dāng)再一次去獲取 SP 實例時,會判斷當(dāng)前磁盤文件相對最后一次內(nèi)存修改是否被改動過了穿扳,如果是的話就主動去重新加載磁盤文件衩侥,從而可以做到在多進程環(huán)境下一定的數(shù)據(jù)同步
但是,這種同步本身作用不大纵揍,因為即使此時重新加載磁盤文件了顿乒,后續(xù)修改 SP 值時不同進程中的內(nèi)存數(shù)據(jù)也不會實時同步,且多進程同時修改 SP 值也存在數(shù)據(jù)丟失和數(shù)據(jù)覆蓋的可能泽谨。所以說璧榄,SP 并不支持多進程數(shù)據(jù)共享,MODE_MULTI_PROCESS 也已經(jīng)被廢棄了吧雹,其注釋也推薦使用 ContentProvider 來實現(xiàn)跨進程通信
class ContextImpl extends Context {
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
···
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
//重新去加載磁盤文件
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
}
不支持增量更新
我們知道骨杂,SP 提交數(shù)據(jù)的方法有兩個:commit()
和 apply()
,分別對應(yīng)著同步修改和異步修改雄卷,而這兩種方式對應(yīng)的都是全量更新搓蚪,SP 以文件為最小單位進行修改,即使我們只修改了一個鍵值對丁鹉,這兩個方法也會將所有鍵值對數(shù)據(jù)重新寫入到磁盤文件中妒潭,即 SP 只支持全量更新
我們平時獲取到的 Editor 對象,對應(yīng)的都是 SharedPreferencesImpl 的內(nèi)部類 EditorImpl揣钦。EditorImpl 的每個 putValue 方法都會將傳進來的 key-value 保存在 mModified
中雳灾,暫時還沒有涉及任何文件改動。比較特殊的是 remove
和 clear
兩個方法冯凹,remove
方法會將 this 作為鍵值對的 value谎亩,后續(xù)就通過對比 value 的相等性來知道是要移除鍵值對還是修改鍵值對,clear
方法則只是將 mClear 標記位置為 true
public final class EditorImpl implements Editor {
private final Object mEditorLock = new Object();
@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();
@GuardedBy("mEditorLock")
private boolean mClear = false;
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
@Override
public Editor remove(String key) {
synchronized (mEditorLock) {
//存入當(dāng)前的 EditorImpl 對象
mModified.put(key, this);
return this;
}
}
@Override
public Editor clear() {
synchronized (mEditorLock) {
mClear = true;
return this;
}
}
}
commit()
和apply()
兩個方法都會通過調(diào)用 commitToMemory()
方法拿到修改后的全量數(shù)據(jù)commitToMemory()
采用了 diff 算法宇姚,SP 包含的所有鍵值對數(shù)據(jù)都存儲在 mapToWriteToDisk 中匈庭,Editor 改動到的所有鍵值對數(shù)據(jù)都存儲在 mModified 中。如果 mClear 為 true浑劳,則會先清空 mapToWriteToDisk阱持,然后再遍歷 mModified,將 mModified 中的所有改動都同步給 mapToWriteToDisk魔熏。最終 mapToWriteToDisk 就保存了要重新寫入到磁盤文件中的全量數(shù)據(jù)紊选,SP 會根據(jù) mapToWriteToDisk 完全覆蓋掉舊的 xml 文件
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
boolean keysCleared = false;
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.
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);
}
//拿到內(nèi)存中的全量數(shù)據(jù)
mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList<String>();
listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (mEditorLock) {
//用于標記最終是否改動到了 mapToWriteToDisk
boolean changesMade = false;
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
//清空所有在內(nèi)存中的數(shù)據(jù)
mapToWriteToDisk.clear();
}
keysCleared = true;
//恢復(fù)狀態(tài)啼止,避免二次修改時狀態(tài)錯位
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.
if (v == this || v == null) { //意味著要移除該鍵值對
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k);
} else { //對應(yīng)修改鍵值對值的情況
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);
}
}
//恢復(fù)狀態(tài),避免二次修改時狀態(tài)錯位
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
listeners, mapToWriteToDisk);
}
Clear 的反直覺用法
看以下例子兵罢。按照語義分析的話献烦,最終 SP 中應(yīng)該是只剩下 blog 一個鍵值對才符合直覺,而實際上最終兩個鍵值對都會被保留卖词,且只有這兩個鍵值對被保留下來
val sharedPreferences: SharedPreferences = getSharedPreferences("UserInfo", Context.MODE_PRIVATE)
val edit = sharedPreferences.edit()
edit.putString("name", "業(yè)志陳").clear().putString("blog", "https://juejin.cn/user/923245496518439")
edit.apply()
造成該問題的原因還需要看commitToMemory()
方法巩那。clear()
會將 mClear 置為 true,所以在執(zhí)行到第一步的時候就會將內(nèi)存中的所有鍵值對數(shù)據(jù) mapToWriteToDisk 清空此蜈。當(dāng)執(zhí)行到第二步的時候即横,mModified 中的所有數(shù)據(jù)就都會同步到 mapToWriteToDisk 中,從而導(dǎo)致最終 name 和 blog 兩個鍵值對都會被保留下來裆赵,其它鍵值對都被移除了
所以說东囚,Editor.clear()
之前不應(yīng)該連貫調(diào)用 putValue 語句,這會造成理解和實際效果之間的偏差
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
boolean keysCleared = false;
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.
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);
}
//拿到內(nèi)存中的全量數(shù)據(jù)
mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList<String>();
listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (mEditorLock) {
boolean changesMade = false;
if (mClear) { //第一步
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
//清空所有在內(nèi)存中的數(shù)據(jù)
mapToWriteToDisk.clear();
}
keysCleared = true;
//恢復(fù)狀態(tài)战授,避免二次修改時狀態(tài)錯位
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.
if (v == this || v == null) { //意味著要移除該鍵值對
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k);
} else { //對應(yīng)修改鍵值對值的情況
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);
}
}
//恢復(fù)狀態(tài)扁凛,避免二次修改時狀態(tài)錯位
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
listeners, mapToWriteToDisk);
}
Commit局冰、apply 可能導(dǎo)致 ANR
commit()
方法會通過 commitToMemory()
方法拿到本次修改后的全量數(shù)據(jù)强法,即 MemoryCommitResult蟹肘,然后向 enqueueDiskWrite
方法提交將全量數(shù)據(jù)寫入磁盤文件的任務(wù),在寫入完成前調(diào)用者線程都會由于 CountDownLatch 一直阻塞等待著楣导,方法返回值即本次修改操作的成功狀態(tài)
@Override
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
//拿到修改后的全量數(shù)據(jù)
MemoryCommitResult mcr = commitToMemory();
//提交寫入磁盤文件的任務(wù)
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
//阻塞等待废境,直到 xml 文件寫入完成(不管成功與否)
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;
}
enqueueDiskWrite
方法就是包含了具體的磁盤寫入邏輯的地方了,由于外部可能存在多個線程在同時執(zhí)行 apply()
和 commit()
兩個方法筒繁,而對應(yīng)的磁盤文件只有一個噩凹,所以 enqueueDiskWrite
方法就必須保證寫入操作的有序性,避免數(shù)據(jù)丟失或者覆蓋毡咏,甚至是文件損壞
enqueueDiskWrite
方法的具體邏輯:
- writeToDiskRunnable 使用到了內(nèi)部鎖 mWritingToDiskLock 來保證 writeToFile 操作的有序性栓始,避免多線程競爭
- 對于 commit 操作,如果當(dāng)前只有一個線程在執(zhí)行提交修改的操作的話血当,那么直接在該線程上執(zhí)行 writeToDiskRunnable,流程結(jié)束
- 對于其他情況(apply 操作禀忆、多線程同時 commit 或者 apply)臊旭,都會將 writeToDiskRunnable 提交給 QueuedWork 執(zhí)行
- QueuedWork 內(nèi)部使用到了 HandlerThread 來執(zhí)行 writeToDiskRunnable,HandlerThread 本身也可以保證多個任務(wù)執(zhí)行時的有序性
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) {
//寫入磁盤文件
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) { //commit() 方法會走進這里面
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
//wasEmpty 為 true 說明當(dāng)前只有一個線程在執(zhí)行提交操作箩退,那么就直接在此線程上完成任務(wù)
writeToDiskRunnable.run();
return;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
此外离熏,還有一個比較重要的知識點需要注意下。在 writeToFile 方法中會對本次任務(wù)進行校驗戴涝,避免連續(xù)多次執(zhí)行無效的磁盤任務(wù)滋戳。當(dāng)中钻蔑,mDiskStateGeneration 代表的是最后一次成功寫入磁盤文件時的任務(wù)版本號,mCurrentMemoryStateGeneration 是當(dāng)前內(nèi)存中最新的修改記錄版本號奸鸯,mcr.memoryStateGeneration 是本次要執(zhí)行的任務(wù)的版本號咪笑。通過兩次版本號的對比,就避免了在連續(xù)多次 commit 或者 apply 時造成重復(fù)執(zhí)行 I/O 操作的情況娄涩,而是只會執(zhí)行最后一次窗怒,避免了無效的 I/O 任務(wù)
@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
···
if (fileExists) {
boolean needsWrite = false;
// Only need to write if the disk state is older than this commit
//判斷版本號
if (mDiskStateGeneration < mcr.memoryStateGeneration) {
if (isFromSyncCommit) {
needsWrite = true;
} else {
synchronized (mLock) {
// No need to persist intermediate states. Just wait for the latest state to
// be persisted.
//判斷版本號
if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
needsWrite = true;
}
}
}
}
if (!needsWrite) {
//當(dāng)前版本號并非最新,無需執(zhí)行蓄拣,直接返回即可
mcr.setDiskWriteResult(false, true);
return;
}
···
}
再回過頭看 commit()
方法扬虚。不管該方法關(guān)聯(lián)的 writeToDiskRunnable 最終是在本線程還是 HandlerThread 中執(zhí)行,await()
方法都會使得本線程阻塞等待直到 writeToDiskRunnable 執(zhí)行完畢球恤,從而實現(xiàn)了 commit()
同步提交的效果
@Override
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
//拿到修改后的全量數(shù)據(jù)
MemoryCommitResult mcr = commitToMemory();
//提交寫入磁盤文件的任務(wù)
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
//阻塞等待辜昵,直到 xml 文件寫入完成(不管成功與否)
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;
}
而對于 apply()
方法,其本身具有異步提交的含義咽斧,I/O 操作應(yīng)該都是交由給了子線程來執(zhí)行才對堪置,按道理來說只需要調(diào)用 enqueueDiskWrite
方法提交任務(wù)且不等待任務(wù)完成即可,可實際上apply()
方法反而要比commit()
方法復(fù)雜得多
apply()
方法包含一個 awaitCommit 任務(wù)收厨,用于阻塞其執(zhí)行線程直到磁盤任務(wù)執(zhí)行完畢晋柱,而 awaitCommit 又被包裹在 postWriteRunnable 中一起提交給了 enqueueDiskWrite
方法,enqueueDiskWrite
方法又會在 writeToDiskRunnable 執(zhí)行完畢后執(zhí)行 enqueueDiskWrite
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
//阻塞線程直到磁盤任務(wù)執(zhí)行完畢
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() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
//提交任務(wù)
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);
}
單獨看以上邏輯會顯得十分奇怪诵叁,從上文就可以得知 writeToDiskRunnable 最終是會交由 HandlerThread 來執(zhí)行的雁竞,那按照流程看 awaitCommit 最終也是會由 HandlerThread 調(diào)用,那么 awaitCommit 的等待操作就顯得十分奇怪了拧额,因為 awaitCommit 肯定是會在磁盤任務(wù)執(zhí)行完畢才被調(diào)用碑诉,就相當(dāng)于 HandlerThread 在自己等待自己執(zhí)行完畢。此外侥锦,HandlerThread 屬于子線程进栽,按道理來說子線程即使執(zhí)行了耗時操作也不會導(dǎo)致主線程 ANR 才對
要理解以上操作,還需要再看看 ActivityThread 這個類恭垦。當(dāng) Service 和 Activity 的生命周期處于 handleStopService()
快毛、handlePauseActivity()
、handleStopActivity()
的時候番挺,ActivityThread 會調(diào)用 QueuedWork.waitToFinish()
方法
private void handleStopService(IBinder token) {
Service s = mServices.remove(token);
if (s != null) {
try {
···
//重點
QueuedWork.waitToFinish();
···
} catch (Exception e) {
···
}
} else {
Slog.i(TAG, "handleStopService: token=" + token + " not found.");
}
//Slog.i(TAG, "Running services: " + mServices);
}
QueuedWork.waitToFinish()
方法會主動去執(zhí)行所有的磁盤寫入任務(wù)唠帝,并執(zhí)行所有的 postWriteRunnable,這就造成了 Activity 或 Service 在切換生命周期的過程中有可能因為存在大量的磁盤寫入任務(wù)而被阻塞住玄柏,最終導(dǎo)致 ANR
public static void waitToFinish() {
long startTime = System.currentTimeMillis();
boolean hadMessages = false;
Handler handler = getHandler();
synchronized (sLock) {
if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
// Delayed work will be processed at processPendingWork() below
handler.removeMessages(QueuedWorkHandler.MSG_RUN);
if (DEBUG) {
hadMessages = true;
Log.d(LOG_TAG, "waiting");
}
}
// We should not delay any work as this might delay the finishers
sCanDelay = false;
}
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
try {
//執(zhí)行所有的磁盤寫入任務(wù)
processPendingWork();
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
try {
//執(zhí)行所有的 postWriteRunnable
while (true) {
Runnable finisher;
synchronized (sLock) {
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}
finisher.run();
}
} finally {
sCanDelay = true;
}
synchronized (sLock) {
long waitTime = System.currentTimeMillis() - startTime;
if (waitTime > 0 || hadMessages) {
mWaitTimes.add(Long.valueOf(waitTime).intValue());
mNumWaits++;
if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {
mWaitTimes.log(LOG_TAG, "waited: ");
}
}
}
}
ActivityThread 為什么要主動去觸發(fā)執(zhí)行所有的磁盤寫入任務(wù)我無從得知襟衰,字節(jié)技術(shù)跳動團隊給出的猜測是:Google 在 Activity 和 Service 調(diào)用 onStop 之前阻塞主線程來處理 SP,我們能猜到的唯一原因是盡可能的保證數(shù)據(jù)的持久化粪摘。因為如果在運行過程中產(chǎn)生了 crash瀑晒,也會導(dǎo)致 SP 未持久化绍坝,持久化本身是 IO 操作,也會失敗
綜上所述苔悦,由于 SP 本身只支持全量更新轩褐,如果 SP 文件很大,即使是小數(shù)據(jù)量的 apply/commit 操作也有可能導(dǎo)致 ANR
正反面
SharedPreferencesImpl 在不同的系統(tǒng)版本中有著比較大的差別间坐,例如 writeToFile 方法對于任務(wù)版本號的校驗也是從 8.0 系統(tǒng)開始的灾挨,在 8.0 系統(tǒng)之前對于連續(xù)的 commit 和 apply 每次都會觸發(fā) I/O 操作,所以在 8.0 系統(tǒng)之前 ANR 問題會更加容易復(fù)現(xiàn)竹宋。我們需要根據(jù)系統(tǒng)版本來看待以上列舉出來的各個缺陷
需要強調(diào)的是劳澄,SP 本身的定位是輕量級數(shù)據(jù)存儲,設(shè)計初衷是用于存儲簡單的數(shù)據(jù)結(jié)構(gòu)(基本數(shù)據(jù)類型)蜈七,且提供了按模塊分區(qū)存儲的功能秒拔。如果開發(fā)者能夠嚴格遵守這一個規(guī)范的話,那么其實以上所述的很多“缺陷”都是可以避免的飒硅。而 SP 之所以現(xiàn)在看起來問題很多砂缩,也是因為如今大部分應(yīng)用的業(yè)務(wù)比以前復(fù)雜太多了,有些時候為了方便就直接用來存儲非常復(fù)雜的數(shù)據(jù)結(jié)構(gòu)三娩,或者是沒有做好數(shù)據(jù)分區(qū)存儲庵芭,導(dǎo)致單個文件過大,這才是造成問題的主要原因
如何做好持久化
以下的示例代碼估計是很多開發(fā)者的噩夢
val sharedPreference = getSharedPreferences("user_preference", Context.MODE_PRIVATE)
val name = sharedPreference.getString("name", "")
以上代碼存在什么問題呢雀监?我覺得至少有五點:
- 強引用到了 SP双吆,導(dǎo)致后續(xù)需要切換存儲庫時需要全局搜索替換,工作量非常大
- key 值難維護会前,每次獲取 value 時都需要顯式聲明 key 值
- 可讀性差好乐,鍵值對的含義基本只能靠 key 值進行表示
- 只支持基本數(shù)據(jù)類型,在存取自定義數(shù)據(jù)類型時存在很多重復(fù)工作瓦宜。要向 SP 存入自定義的 - JavaBean 對象時蔚万,只能將 Bean 對象轉(zhuǎn)為 Json 字符串后存入 SP,在取值時再手動反序列化
- 數(shù)據(jù)類型不明確临庇,基本只能靠注釋來引導(dǎo)開發(fā)者使用正確的數(shù)據(jù)類型
開發(fā)者往往是會聲明出各種 SpUtils 類進行多一層封裝反璃,但也沒法徹底解決以上問題。SP 的確是存在著一些設(shè)計缺陷假夺,但對于大部分應(yīng)用開發(fā)者來說其實并沒有多少選擇淮蜈,我們只能選擇用或者不用,并沒有多少余地可以來解決或者避免其存在的問題侄泽,我們往往只能在遇到問題后切換到其它的持久化存儲方案
目前有兩個比較知名的持久化存儲方案:Jetpack DataStore 和騰訊的 MMKV,我們當(dāng)然可以選擇將項目中的 SP 切換為這兩個庫之一蜻韭,但這也不禁讓人想到一個問題悼尾,如果以后這兩個庫也遇到了問題甚至是直接被廢棄了柿扣,難道我們又需要再來全局替換一遍嗎?我們應(yīng)該如何設(shè)計才能使得每次的替換成本降到最低呢闺魏?
在我看來未状,開發(fā)者在為項目引入一個新的依賴庫之前就應(yīng)該為以后移除該庫做好準備,做好接口隔離析桥,屏蔽具體的底層邏輯(當(dāng)然司草,也不是每個依賴庫都可以做到)。筆者的項目之前也是使用 SP 來存儲配置信息泡仗,后來我也將其切換到了 MMKV埋虹,下面就來介紹下筆者當(dāng)時是如何設(shè)計存儲結(jié)構(gòu)避免硬編碼的
目前的效果
我將應(yīng)用內(nèi)所有需要存儲的鍵值對數(shù)據(jù)分為了三類:用戶強關(guān)聯(lián)數(shù)據(jù)、應(yīng)用配置數(shù)據(jù)娩怎、不可二次變更的數(shù)據(jù)搔课。每一類數(shù)據(jù)的存儲區(qū)域各不相同,互不影響截亦。進行數(shù)據(jù)分組的好處就在于可以根據(jù)需要來清除特定數(shù)據(jù)爬泥,例如當(dāng)用戶退登后我們可以只清除 UserKVHolder,而 PreferenceKVHolder 和 FinalKVHolder 則可以一直保留
IKVHolder 接口定義了基本的存取方法崩瓤,MMKVKVHolder 通過 MMKV 實現(xiàn)了具體的存儲邏輯
//和用戶強綁定的數(shù)據(jù)袍啡,在退出登錄時需要全部清除,例如 UserBean
//設(shè)置 encryptKey 以便加密存儲
private val UserKVHolder: IKVHolder = MMKVKVHolder("user", "加密key")
//和用戶不強關(guān)聯(lián)的數(shù)據(jù)却桶,在退出登錄時無需清除境输,例如夜間模式、字體大小等
private val PreferenceKVHolder: IKVHolder = MMKVKVHolder("preference")
//用于存儲不會二次變更只用于歷史溯源的數(shù)據(jù)肾扰,例如應(yīng)用首次安裝的時間畴嘶、版本號、版本名等
private val FinalKVHolder: IKVHolder = MMKVKVFinalHolder("final")
之后我們就可以利用 Kotlin 強大的語法特性來定義鍵值對了
例如集晚,對于和用戶強關(guān)聯(lián)的數(shù)據(jù)窗悯,每個鍵值對都定義為 UserKV 的一個屬性字段,鍵值對的含義和作用通過屬性名來進行標識偷拔,且鍵值對的 key 必須和屬性名保持一致蒋院,這樣可以避免 key 值重復(fù)。每個 getValue 操作也都支持設(shè)置默認值莲绰。IKVHolder 內(nèi)部通過 Gson 來實現(xiàn)序列化和反序列化欺旧,這樣 UserKV 就可以直接存儲 JavaBean、JavaBeanList蛤签,Map 等數(shù)據(jù)結(jié)構(gòu)了
object UserKV : IKVHolder by UserKVHolder {
var name: String
get() = get("name", "")
set(value) = set("name", value)
var blog: String
get() = get("blog", "")
set(value) = set("blog", value)
var userBean: UserBean?
get() = getBeanOrNull("userBean")
set(value) = set("userBean", value)
var userBeanOfDefault: UserBean
get() = getBeanOrDefault(
"userBeanOfDefault",
UserBean("業(yè)志陳", "https://juejin.cn/user/923245496518439")
)
set(value) = set("userBeanOfDefault", value)
var userBeanList: List<UserBean>
get() = getBean("userBeanList")
set(value) = set("userBeanList", value)
var map: Map<Int, String>
get() = getBean("map")
set(value) = set("map", value)
}
此外辞友,我們也可以在 setValue 方法中對 value 進行校驗,避免無效值
object UserKV : IKVHolder by UserKVHolder {
var age: Int
get() = get("age", 0)
set(value) {
if (value <= 0) {
return
}
set("age", value)
}
}
之后我們在存取值時,就相當(dāng)于在直接讀寫 UserKV 的屬性值称龙,也支持動態(tài)指定 Key 進行賦值取值留拾,在易用性和可讀性上相比 SharedPreferences 都有很大的提升,且對于外部來說完全屏蔽了具體的存儲實現(xiàn)邏輯
//存值
UserKV.name = "xxxxx"
UserKV.blog = "https://juejin.cn/user/923245496518439"
//取值
val name = UserKV.name
val blog = UserKV.blog
//動態(tài)指定 Key 進行賦值和取值
UserKV.set("name", "xxx")
val name = UserKV.get("name", "")
如何設(shè)計的
首先鲫尊,IKVHolder 定義了基本的存取方法痴柔,除了需要支持基本數(shù)據(jù)類型外,還需要支持自定義的數(shù)據(jù)類型疫向。依靠 Kotlin 的 擴展函數(shù) 和 內(nèi)聯(lián)函數(shù) 這兩個語法特性咳蔚,我們在存取自定義類型時都無需聲明泛型類型,使用上十分簡潔搔驼。JsonHolder 則是通過 Gson 實現(xiàn)了基本的序列化和反序列化方法
interface IKVHolder {
companion object {
inline fun <reified T> IKVHolder.getBean(key: String): T {
return JsonHolder.toBean(get(key, ""))
}
inline fun <reified T> IKVHolder.getBeanOrNull(key: String): T? {
return JsonHolder.toBeanOrNull(get(key, ""))
}
inline fun <reified T> IKVHolder.getBeanOrDefault(key: String, defaultValue: T): T {
return JsonHolder.toBeanOrDefault(get(key, ""), defaultValue)
}
fun toJson(ob: Any?): String {
return JsonHolder.toJson(ob)
}
}
//數(shù)據(jù)分組谈火,用于標明不同范圍內(nèi)的數(shù)據(jù)緩存
val keyGroup: String
fun verifyBeforePut(key: String, value: Any?): Boolean
fun get(key: String, default: Int): Int
fun set(key: String, value: Int)
fun <T> set(key: String, value: T?)
fun containsKey(key: String): Boolean
fun removeKey(vararg keys: String)
fun allKeyValue(): Map<String, Any?>
fun clear()
···
}
BaseMMKVKVHolder 實現(xiàn)了 IKVHolder 接口,內(nèi)部引入了 MMKV 作為具體的持久化存儲方案
/**
* @param selfGroup 用于指定數(shù)據(jù)分組匙奴,不同分組下的數(shù)據(jù)互不關(guān)聯(lián)
* @param encryptKey 加密 key堆巧,如果為空則表示不進行加密
*/
sealed class BaseMMKVKVHolder constructor(
selfGroup: String,
encryptKey: String
) : IKVHolder {
final override val keyGroup: String = selfGroup
override fun verifyBeforePut(key: String, value: Any?): Boolean {
return true
}
private val kv: MMKV? = if (encryptKey.isBlank()) MMKV.mmkvWithID(
keyGroup,
MMKV.MULTI_PROCESS_MODE
) else MMKV.mmkvWithID(keyGroup, MMKV.MULTI_PROCESS_MODE, encryptKey)
override fun set(key: String, value: Int) {
if (verifyBeforePut(key, value)) {
kv?.putInt(key, value)
}
}
override fun <T> set(key: String, value: T?) {
if (verifyBeforePut(key, value)) {
if (value == null) {
removeKey(key)
} else {
set(key, toJson(value))
}
}
}
override fun get(key: String, default: Int): Int {
return kv?.getInt(key, default) ?: default
}
override fun containsKey(key: String): Boolean {
return kv?.containsKey(key) ?: false
}
override fun removeKey(vararg keys: String) {
kv?.removeValuesForKeys(keys)
}
override fun allKeyValue(): Map<String, Any?> {
val map = mutableMapOf<String, Any?>()
kv?.allKeys()?.forEach {
map[it] = getObjectValue(kv, it)
}
return map
}
override fun clear() {
kv?.clearAll()
}
···
}
BaseMMKVKVHolder 有兩個子類,其區(qū)別只在于 MMKVKVFinalHolder 保存鍵值對后無法再次更改值泼菌,用于存儲不會二次變更只用于歷史溯源的數(shù)據(jù)谍肤,例如應(yīng)用首次安裝時的時間戳、版本號哗伯、版本名等
/**
* @param selfGroup 用于指定數(shù)據(jù)分組荒揣,不同分組下的數(shù)據(jù)互不關(guān)聯(lián)
* @param encryptKey 加密 key,如果為空則表示不進行加密
*/
class MMKVKVHolder constructor(selfGroup: String, encryptKey: String = "") :
BaseMMKVKVHolder(selfGroup, encryptKey)
/**
* 存儲后值無法二次變更
* @param selfGroup 用于指定數(shù)據(jù)分組焊刹,不同分組下的數(shù)據(jù)互不關(guān)聯(lián)
* @param encryptKey 加密 key系任,如果為空則表示不進行加密
*/
class MMKVKVFinalHolder constructor(selfGroup: String, encryptKey: String = "") :
BaseMMKVKVHolder(selfGroup, encryptKey) {
override fun verifyBeforePut(key: String, value: Any?): Boolean {
return !containsKey(key)
}
}
通過接口隔離,UserKV 就完全不會接觸到具體的存儲實現(xiàn)機制了虐块,對于開發(fā)者來說也只是在讀寫 UserKV 的一個屬性字段而已俩滥,當(dāng)后續(xù)我們需要替換存儲方案時,也只需要去改動 MMKVKVHolder 的內(nèi)部實現(xiàn)即可贺奠,上層應(yīng)用完全不需要進行任何改動
KVHolder
KVHolder 的實現(xiàn)思路還是十分簡單的霜旧,再加上 Kotlin 本身強大的語法特性就進一步提高了易用性和可讀性 ???? 我也將其發(fā)布為開源庫,感興趣的讀者可以直接遠程導(dǎo)入依賴
allprojects {
repositories {
maven { url "https://jitpack.io" }
}
}
dependencies {
implementation 'com.github.leavesC:KVHolder:latest_version'
}
GitHub 點擊這里:KVHolder