Android?SharedPreferences性能瓶頸解析
正文
想必大家對SharedPreferences都已經(jīng)很熟悉了,大型應(yīng)用使用SharedPreferences開發(fā)很容易出現(xiàn)性能瓶頸,相信很多開發(fā)者已經(jīng)遷移到MMKV進(jìn)行配置存儲
說到MMKV我們總是會(huì)看到如下這張圖

在模擬1000次寫入的情況下,MMKV大幅度領(lǐng)先SharedPreferences,我們都知道MMKV使用了mmap方式進(jìn)行存儲,而SharedPreferences還是使用傳統(tǒng)的文件系統(tǒng),以xml的方式進(jìn)行配置存儲,mmap確實(shí)具備較好的性能和穩(wěn)定性,但是真的兩種不同的存儲方式可以帶來如此巨大的性能差異嗎?
測試
因此我編寫代碼進(jìn)行了一次測試
findViewById(R.id.test5).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
long time2 = System.currentTimeMillis();
SharedPreferences mSharedPreferences = WebTurboConfiguration.getInstance().mContext.getSharedPreferences(WebTurboConfigSp.Key.SHARED_PREFS_FILE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = mSharedPreferences.edit();
for (int i = 0; i < 1000; i++) {
editor.putString(i + "", 1000 + "");
editor.apply();
}
long time3 = System.currentTimeMillis();
Log.e("模擬寫入", "sp存儲耗時(shí) = " + (time3 - time2));
MMKV mmkv = MMKV.defaultMMKV();
for (int i = 0; i < 1000; i++) {
mmkv.putString(i + "", 1000 + "");
}
long time4 = System.currentTimeMillis();
Log.e("模擬寫入", "mmkv 存儲耗時(shí) = " + (time4 - time3));
}
});
輸出如下
E/模擬寫入: sp存儲耗時(shí) = 82ms
E/模擬寫入: mmkv 存儲耗時(shí) = 6ms
MMKV確實(shí)性能顯著強(qiáng)于SharedPreferences
apply方法的注釋
SharedPreferences在使用的時(shí)候是推薦使用apply進(jìn)行保存,我們來看一下apply方法的注釋

注釋中明確說明apply方法是先將存儲數(shù)據(jù)提交到內(nèi)存,然后異步進(jìn)行磁盤寫入,既然是異步寫入,理論上IO不會(huì)拖后腿,我們可以認(rèn)為時(shí)間都被消耗在了將數(shù)據(jù)提交到內(nèi)存上,在寫入內(nèi)存上面SharedPreferences與MMKV會(huì)有這么大的性能差距嗎?
這激起了我的興趣
我使用AS自帶的性能分析工具對SharedPreferences存儲過程進(jìn)行一次trace分析 分析圖如下

可以輕松的從圖中看到
數(shù)據(jù)存儲put方法的主要耗時(shí)在puMapEntries上
代碼調(diào)用如下
SharedPreferences的實(shí)際實(shí)現(xiàn)代碼在SharedPreferencesImpl中
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
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() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
主要看
final MemoryCommitResult mcr = commitToMemory();
代碼比較長
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
boolean keysCleared = false;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
if (mDiskWritesInFlight > 0) {
mMap = new HashMap<String, Object>(mMap);
}
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;
mapToWriteToDisk.clear();
}
keysCleared = true;
mClear = false;
}
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
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, keysCleared, keysModified,
listeners, mapToWriteToDisk);
}
執(zhí)行邏輯
step1:可能需要對現(xiàn)有的數(shù)據(jù)mMap進(jìn)行一次深度拷貝,生成新的mMap對象
step2:對存儲了已修改數(shù)據(jù)的map(mModified)進(jìn)行遍歷,寫入mMap
step3:返回包含了全部數(shù)據(jù)的map用于存入文件系統(tǒng)
上文提到的大量耗時(shí)的puMapEntries方法就發(fā)生在step1中map的深度拷貝代碼中
if (mDiskWritesInFlight > 0) {
mMap = new HashMap<String, Object>(mMap);
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
為什么step1中說可能需要進(jìn)行一次深度拷貝呢,因?yàn)閙DiskWritesInFlight的值,在有配置需要寫入時(shí),他就會(huì)+1,只有完全寫入磁盤,也就是此次配置已經(jīng)被持久化,mDiskWritesInFlight才會(huì)-1,也就是說深度拷貝在上文提到的1000次寫入的場景下是一定會(huì)發(fā)生的,除了第一次可能不需要深度拷貝,后面999次大概率會(huì)發(fā)生深度拷貝,因?yàn)樵谡麄€(gè)1000次的寫入過程中,線程一直在不斷的將配置寫入磁盤,一直到1000次apply完成,數(shù)據(jù)可能還需要一段時(shí)間才能往磁盤里面寫完
我們代碼來模擬深度拷貝的場景,看深度拷貝map到底有多耗時(shí),在代碼中我們模擬了1000次深度拷貝
E/模擬寫入: map深度拷貝耗時(shí) = 52ms
E/模擬寫入: sp存儲耗時(shí) = 59ms
E/模擬寫入: mmkv 存儲耗時(shí) = 4ms
可以看到1000次深度拷貝的耗時(shí)已經(jīng)接近SP1000次寫入的耗時(shí)
因此我們得到如下結(jié)論 在開發(fā)者使用SharedPreferences的apply方法進(jìn)行存儲時(shí),高頻次的apply調(diào)用會(huì)導(dǎo)致每次apply時(shí)進(jìn)行map的深度拷貝,導(dǎo)致耗時(shí),如果只是一次調(diào)用,或者低頻次的調(diào)用,那么SharedPreferences依然可以具備較好的性能
下面是一次調(diào)用的模擬,可以看到單次場景下與MMKV的性能差距不明顯
E/模擬寫入: sp存儲耗時(shí) = 231192ns
E/模擬寫入: mmkv 存儲耗時(shí) = 229154ns
那么如果需要高頻次寫入SharedPreferences,如何保證較好的性能呢,比如在一個(gè)循環(huán)中寫入SharedPreferences,那就要想辦法避免map被頻繁的深度拷貝,解決辦法就是多次put完成后再apply
示例代碼如下
findViewById(R.id.test5).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
long time1 = System.currentTimeMillis();
HashMap<String, String> map = new HashMap<>();
for (int i = 0; i < 1000; i++) {
map.put(i + "", 1000 + "");
new HashMap<>(map);
}
long time2 = System.currentTimeMillis();
Log.e("模擬寫入", "map深度拷貝耗時(shí) = " + (time2 - time1));
SharedPreferences mSharedPreferences = WebTurboConfiguration.getInstance().mContext.getSharedPreferences(WebTurboConfigSp.Key.SHARED_PREFS_FILE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = mSharedPreferences.edit();
for (int i = 0; i < 1000; i++) {
editor.putString(i + "", 1000 + "");
}
editor.apply();
long time3 = System.currentTimeMillis();
Log.e("模擬寫入", "sp存儲耗時(shí) = " + (time3 - time2));
MMKV mmkv = MMKV.defaultMMKV();
for (int i = 0; i < 1000; i++) {
mmkv.putString(i + "", 1000 + "");
}
long time4 = System.currentTimeMillis();
Log.e("模擬寫入", "mmkv 存儲耗時(shí) = " + (time4 - time3));
}
});
輸出結(jié)果如下,SharedPreferences的存儲耗時(shí)甚至低于MMKV
E/模擬寫入: map深度拷貝耗時(shí) = 55
E/模擬寫入: sp存儲耗時(shí) = 1
E/模擬寫入: mmkv 存儲耗時(shí) = 4
本文只針對循環(huán)保存配置這一種場景進(jìn)行分析,無論如何使用,MMKV性能強(qiáng)于SharedPreferences是不爭的事實(shí),如果開發(fā)者開發(fā)的只是一個(gè)小工具,小應(yīng)用,推薦使用SharedPreferences,他足夠的輕量,如果開發(fā)商用中大型應(yīng)用,MMKV依然是最好的選擇,至于jetpack中的DataStore,并未使用過,不做評價(jià)
以上就是Android SharedPreferences性能瓶頸解析的詳細(xì)內(nèi)容,更多關(guān)于Android SharedPreferences性能瓶頸的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android 日常開發(fā)總結(jié)的60條技術(shù)經(jīng)驗(yàn)
這篇文章主要介紹了Android日常開發(fā)總結(jié)的技術(shù)經(jīng)驗(yàn)60條,需要的朋友可以參考下2016-03-03
Android編程四大組件之Activity用法實(shí)例分析
這篇文章主要介紹了Android編程四大組件之Activity用法,實(shí)例分析了Activity的創(chuàng)建,生命周期,內(nèi)存管理及啟動(dòng)模式等,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2016-01-01
Android響應(yīng)事件onClick方法的五種實(shí)現(xiàn)方式小結(jié)
本篇文章主要介紹了Android響應(yīng)onClick方法的五種實(shí)現(xiàn)方式小結(jié),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-03-03
Android開發(fā)使用HttpURLConnection進(jìn)行網(wǎng)絡(luò)編程詳解【附源碼下載】
這篇文章主要介紹了Android開發(fā)使用HttpURLConnection進(jìn)行網(wǎng)絡(luò)編程的方法,結(jié)合實(shí)例形式分析了Android基于HttpURLConnection實(shí)現(xiàn)顯示圖片與文本功能,涉及Android布局、文本解析、數(shù)據(jù)傳輸、權(quán)限控制等相關(guān)操作技巧,需要的朋友可以參考下2018-01-01
詳細(xì)分析Android中onTouch事件傳遞機(jī)制
相信不少朋友在剛開始學(xué)習(xí)Android的時(shí)候,對于onTouch相關(guān)的事件一頭霧水。分不清onTouch(),onTouchEvent()和OnClick()之間的關(guān)系和先后順序,所以覺得有必要搞清onTouch事件傳遞的原理。經(jīng)過一段時(shí)間的琢磨以及相關(guān)博客的介紹,這篇文章就給大家詳細(xì)的分析介紹下。2016-10-10
Android 動(dòng)態(tài)改變布局實(shí)例詳解
這篇文章主要介紹了Android 動(dòng)態(tài)改變布局實(shí)例詳解的相關(guān)資料,這里舉例說明如何實(shí)現(xiàn)動(dòng)態(tài)改變布局的例子,幫助大家學(xué)習(xí)理解,需要的朋友可以參考下2016-11-11
Android自定義控件(實(shí)現(xiàn)狀態(tài)提示圖表)
本篇文章主要介紹了android實(shí)現(xiàn)狀態(tài)提示圖表的功能,實(shí)現(xiàn)了動(dòng)態(tài)圖表的顯示,有需要的朋友可以了解一下。2016-11-11

