我們經(jīng)常使用的SharedPreferences其實是存在很多缺陷的,主要表現(xiàn)在
- 占用內(nèi)存
- getValue時可能導(dǎo)致ANR
- 不支持多進(jìn)程
- 不支持局部更新
- commit或apply都可能導(dǎo)致ANR
以下參考安卓源碼的基礎(chǔ)上海洼,使用大白話和部分代碼片段和大家一起探討分享纵装。
占用內(nèi)存
final class SharedPreferencesImpl implements SharedPreferences {
......
//構(gòu)造方法
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
//從磁盤里獲取xml里的數(shù)據(jù)
startLoadFromDisk();
}
.....
}
我們都知道Context的上下文實現(xiàn)是依靠ContextImpl這個類擂煞,而我們的SharedPreferences的實現(xiàn)是依靠SharedPreferencesImpl類骑脱,
ContextImpl.java
/**
* Map from package name, to preference name, to cached preferences.
*/
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
在我們的ContextImpl類中存在一個靜態(tài)的ArrayMap對象用于緩存當(dāng)前packageName下的所有sp文件對象昆咽,
但是在這個類里面我們可以看到緩存數(shù)組的探空 初始化和賦值,但卻沒有對數(shù)組對象里的數(shù)據(jù)進(jìn)行移除或者釋放的操作铛碑,
由此我們也就可以知道狠裹,在我們APP運行的過程中虽界,APP對應(yīng)包目錄下的sp文件都會被緩存到方法區(qū)里去,
而這種機制的話會導(dǎo)致很占內(nèi)存涛菠,而且寧愿OOM也不會主動釋放內(nèi)存空間莉御。
getValue的時候可能導(dǎo)致線程阻塞或ANR
在我們的SharedPreferencesImpl構(gòu)造函數(shù)里,會啟動一個子線程去加載磁盤文件俗冻,把xml文件轉(zhuǎn)換成map對象礁叔,如果文件很大或者線程調(diào)度沒有馬上啟動這個線程的話,那么這個加載的操作需要一段時間后才能執(zhí)行完成迄薄,
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
而假如我們剛好初始化的時候緊接著去getValue的話琅关,getValue里面又會通過awaitLoadedLocked方法來校驗是否要阻塞外部線程,
private void awaitLoadedLocked() {
if (!mLoaded) {
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
//如果沒有加載完成 就一直持有鎖
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
確保取值操作前一定是執(zhí)行完成了file文件的加載和轉(zhuǎn)換成功讥蔽,最后在磁盤加載完成時才會notify操作 把我們外部讀取value的線程給喚醒涣易。
在上述的操作場景都是我們APP經(jīng)常會出現(xiàn)的,同時當(dāng)我們sp離數(shù)據(jù)存儲量很大的話冶伞,那這個磁盤加載并阻塞外部線程的時間會比較大 直接就導(dǎo)致了我們主線程獲取sp值的時候直接就芭比Q anr了新症。
不支持多進(jìn)程
名義上我們在獲取sp實例的時候可以傳參支持多進(jìn)程模式,但這個mode參數(shù)也只是起到一個多進(jìn)程數(shù)據(jù)同步的作用响禽,
static void setFilePermissionsFromMode(String name, int mode,
int extraPermissions) {
int perms = FileUtils.S_IRUSR|FileUtils.S_IWUSR
|FileUtils.S_IRGRP|FileUtils.S_IWGRP
|extraPermissions;
if ((mode&MODE_WORLD_READABLE) != 0) {
perms |= FileUtils.S_IROTH;
}
if ((mode&MODE_WORLD_WRITEABLE) != 0) {
perms |= FileUtils.S_IWOTH;
}
FileUtils.setPermissions(name, perms, -1, -1);
}
這里的同步是指訪問這個sp實例的時候徒爹,會判斷當(dāng)前磁盤文件相對最后一次內(nèi)存修改是否被改動過,如果是的話就重新加載磁盤文件再同步到緩存上芋类,
public static int setPermissions(String path, int mode, int uid, int gid) {
try {
Os.chmod(path, mode);
} catch (ErrnoException e) {
Slog.w(TAG, "Failed to chmod(" + path + "): " + e);
return e.errno;
}
if (uid >= 0 || gid >= 0) {
try {
Os.chown(path, uid, gid);
} catch (ErrnoException e) {
Slog.w(TAG, "Failed to chown(" + path + "): " + e);
return e.errno;
}
}
return 0;
}
但這種同步的作用不大隆嗅,因為當(dāng)多進(jìn)程同時修改sp值,但不同進(jìn)程里的內(nèi)存數(shù)據(jù)也不會實時同步侯繁,而且同時修改sp數(shù)據(jù)也會導(dǎo)致數(shù)據(jù)丟失和覆蓋的可能胖喳。
不支持局部更新
apply
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
//這個任務(wù)最終在ActivityThread里的 handleStopService handlePauseActivity handleStopActivity方法里執(zhí)行
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
// 最終調(diào)用QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
//把這個任務(wù)加入到ActivityThread中的QueueWork列表里
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// changes reflected in memory.
notifyListeners(mcr);
}
我們的同步修改commit方法 和異步修改apply方法都是全量更新,也就是即使我們修改的止損一個鍵值對巫击,它也會把數(shù)據(jù)重寫寫入到磁盤文件中禀晓,這樣就會導(dǎo)致不必要的內(nèi)存開銷。
commit或apply都可能導(dǎo)致ANR
在commit和apply的時候還有一個更致命的問題就是他們也會導(dǎo)致ANR坝锰。
這個主要是因為在調(diào)用commit和apply都會執(zhí)行到一個enqueueDiskWrite操作粹懒,這個操作會把當(dāng)前修改sp內(nèi)存數(shù)據(jù)同步到Disk磁盤的任務(wù)加入到ActivityThread里的一個任務(wù)鏈表集合中, 那么我們肯定會想這個磁盤同步任務(wù)什么時候才會最終完成呢顷级,
其實它是需要等到我們的應(yīng)用中service在stop的時候凫乖,或者activity暫停或停止的時候,才會for循環(huán)上面提到的任務(wù)鏈表集合任務(wù)帽芽,最終完成內(nèi)存數(shù)據(jù)到磁盤數(shù)據(jù)的删掀。 那這樣的話會因為有大量的讀寫同步到磁盤的任務(wù)導(dǎo)致activity或者service切換生命周期的時候被阻塞住了,最終導(dǎo)致了ANR导街。
--》handleStopActivity方法(ActivityThread)
--》QueuedWork.waitToFinish()
--》 processPendingWork(); 再到下面最終執(zhí)行磁盤回寫任務(wù)
for (Runnable w : work) {
w.run();
}
綜上披泪,經(jīng)過這些分析想必我們對SharedPreferences有個更了解的地方。
安卓官方推薦我們可以考慮使用jetpack里的DataStore ,或者可以考慮使用騰訊團隊開發(fā)的MMKV框架搬瑰。