Android?SharedPreferences性能瓶頸解析
正文
想必大家對SharedPreferences都已經很熟悉了,大型應用使用SharedPreferences開發(fā)很容易出現性能瓶頸,相信很多開發(fā)者已經遷移到MMKV進行配置存儲
說到MMKV我們總是會看到如下這張圖
在模擬1000次寫入的情況下,MMKV大幅度領先SharedPreferences,我們都知道MMKV使用了mmap方式進行存儲,而SharedPreferences還是使用傳統(tǒng)的文件系統(tǒng),以xml的方式進行配置存儲,mmap確實具備較好的性能和穩(wě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存儲耗時 = " + (time3 - time2)); MMKV mmkv = MMKV.defaultMMKV(); for (int i = 0; i < 1000; i++) { mmkv.putString(i + "", 1000 + ""); } long time4 = System.currentTimeMillis(); Log.e("模擬寫入", "mmkv 存儲耗時 = " + (time4 - time3)); } });
輸出如下
E/模擬寫入: sp存儲耗時 = 82ms
E/模擬寫入: mmkv 存儲耗時 = 6ms
MMKV確實性能顯著強于SharedPreferences
apply方法的注釋
SharedPreferences在使用的時候是推薦使用apply進行保存,我們來看一下apply方法的注釋
注釋中明確說明apply方法是先將存儲數據提交到內存,然后異步進行磁盤寫入,既然是異步寫入,理論上IO不會拖后腿,我們可以認為時間都被消耗在了將數據提交到內存上,在寫入內存上面SharedPreferences與MMKV會有這么大的性能差距嗎?
這激起了我的興趣
我使用AS自帶的性能分析工具對SharedPreferences存儲過程進行一次trace分析 分析圖如下
可以輕松的從圖中看到
數據存儲put方法的主要耗時在puMapEntries上
代碼調用如下
SharedPreferences的實際實現代碼在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:可能需要對現有的數據mMap進行一次深度拷貝,生成新的mMap對象
step2:對存儲了已修改數據的map(mModified)進行遍歷,寫入mMap
step3:返回包含了全部數據的map用于存入文件系統(tǒng)
上文提到的大量耗時的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中說可能需要進行一次深度拷貝呢,因為mDiskWritesInFlight的值,在有配置需要寫入時,他就會+1,只有完全寫入磁盤,也就是此次配置已經被持久化,mDiskWritesInFlight才會-1,也就是說深度拷貝在上文提到的1000次寫入的場景下是一定會發(fā)生的,除了第一次可能不需要深度拷貝,后面999次大概率會發(fā)生深度拷貝,因為在整個1000次的寫入過程中,線程一直在不斷的將配置寫入磁盤,一直到1000次apply完成,數據可能還需要一段時間才能往磁盤里面寫完
我們代碼來模擬深度拷貝的場景,看深度拷貝map到底有多耗時,在代碼中我們模擬了1000次深度拷貝
E/模擬寫入: map深度拷貝耗時 = 52ms
E/模擬寫入: sp存儲耗時 = 59ms
E/模擬寫入: mmkv 存儲耗時 = 4ms
可以看到1000次深度拷貝的耗時已經接近SP1000次寫入的耗時
因此我們得到如下結論 在開發(fā)者使用SharedPreferences的apply方法進行存儲時,高頻次的apply調用會導致每次apply時進行map的深度拷貝,導致耗時,如果只是一次調用,或者低頻次的調用,那么SharedPreferences依然可以具備較好的性能
下面是一次調用的模擬,可以看到單次場景下與MMKV的性能差距不明顯
E/模擬寫入: sp存儲耗時 = 231192ns
E/模擬寫入: mmkv 存儲耗時 = 229154ns
那么如果需要高頻次寫入SharedPreferences,如何保證較好的性能呢,比如在一個循環(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深度拷貝耗時 = " + (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存儲耗時 = " + (time3 - time2)); MMKV mmkv = MMKV.defaultMMKV(); for (int i = 0; i < 1000; i++) { mmkv.putString(i + "", 1000 + ""); } long time4 = System.currentTimeMillis(); Log.e("模擬寫入", "mmkv 存儲耗時 = " + (time4 - time3)); } });
輸出結果如下,SharedPreferences的存儲耗時甚至低于MMKV
E/模擬寫入: map深度拷貝耗時 = 55
E/模擬寫入: sp存儲耗時 = 1
E/模擬寫入: mmkv 存儲耗時 = 4
本文只針對循環(huán)保存配置這一種場景進行分析,無論如何使用,MMKV性能強于SharedPreferences是不爭的事實,如果開發(fā)者開發(fā)的只是一個小工具,小應用,推薦使用SharedPreferences,他足夠的輕量,如果開發(fā)商用中大型應用,MMKV依然是最好的選擇,至于jetpack中的DataStore,并未使用過,不做評價
以上就是Android SharedPreferences性能瓶頸解析的詳細內容,更多關于Android SharedPreferences性能瓶頸的資料請關注腳本之家其它相關文章!
相關文章
Android開發(fā)使用HttpURLConnection進行網絡編程詳解【附源碼下載】
這篇文章主要介紹了Android開發(fā)使用HttpURLConnection進行網絡編程的方法,結合實例形式分析了Android基于HttpURLConnection實現顯示圖片與文本功能,涉及Android布局、文本解析、數據傳輸、權限控制等相關操作技巧,需要的朋友可以參考下2018-01-01