請不要濫用SharedPreference
SharedPreference是Android上一種非常易用的輕量級存儲方式,由於其API及其友好,得到了很多很多開發者的青睞。但是,SharedPreference並不是萬能的,如果把它用在不合適的使用場景,那麼將會帶來災難性的後果;本文將講述一些SharedPreference的使用誤區。
存儲超大的value
第一次看到下面這個sp的時候,我的內心是崩潰的:
一個默認的sp有90K,當我打開它的時候,我都快哭了:除了零零星星的幾個很小的key之外,存儲了一個炒雞大的key,這一個key至少佔了其中的89K。知道這是什麼概念嗎?
在小米1S這種手機上,就算獲取這個sp裡面一個很小的key,會花費120+ms!!那個毫不相干的key拖慢了其他所有key的讀取速度!當然,在性能稍好的手機上,這個問題不是特別嚴重。但是要知道,120ms這個是完全不能忍的!
之所以說SharedPreference(下文簡稱sp)是一種輕量級的存儲方式,是它的設計所決定的:sp在創建的時候會把整個文件全部載入進內存,如果你的sp文件比較大,那麼會帶來幾個嚴重問題:
- 第一次從sp中獲取值的時候,有可能阻塞主線程,使界面卡頓、掉幀。
- 解析sp的時候會產生大量的臨時對象,導致頻繁GC,引起界面卡頓。
- 這些key和value會永遠存在於內存之中,佔用大量內存。
也許有童鞋會說,sp的載入不是在子線程么,怎麼會卡住主線程?子線程IO就一定不會阻塞主線程嗎?
下面是默認的sp實現SharedPreferenceImpl這個類的getString函數:
public String getString(String key, @Nullable String defValue) { synchronized (this) { awaitLoadedLocked(); String v = (String)mMap.get(key); return v != null ? v : defValue; }}
繼續看看這個awaitLoadedLocked:
private void awaitLoadedLocked() { while (!mLoaded) { try { wait(); } catch (InterruptedException unused) { } }}
一把鎖就是掛在那裡!!這意味著,如果你直接調用getString,主線程會等待載入sp的那麼線程載入完畢!這不就把主線程卡住了么?
另外,有一個叫訣竅可以節省一下等待的時間:既然getString之類的操作會等待sp載入完成,而載入是在另外一個線程執行的,我們可以讓sp先去載入,做一堆事情,然後再getString!如下:
// 先讓sp去另外一個線程載入SharedPreferences sp = getSharedPreferences("test", MODE_PRIVATE);// 做一堆別的事情setContentView(testSpJson);// ...// OK,這時候估計已經載入完了吧,就算沒完,我們在原本應該等待的時間也做了一些事!String testValue = sp.getString("testKey", null);
更為嚴重的是,被載入進來的這些大對象,會永遠存在於內存之中,不會被釋放。我們看看ContextImpl這個類,在getSharedPreference的時候會把所有的sp放到一個靜態變數裡面緩存起來:
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;}
注意這個static的sSharedPrefsCache,它保存了你所有使用的sp,然後sp裡面有一個成員mMap保存了所有的鍵值對;這樣,你程序中使用到的那些個sp永遠就呆在內存中,是不是不寒而慄?!
所以,請不要在sp裡面存儲炒雞大的key碰到這樣的豬隊友,請讓他自行檢討!!趕緊把自家App檢查一下!!
存儲JSON等特殊符號很多的value
還有一些童鞋,他在sp裡面存json或者HTML;這麼做不是不可以,但是,如果這個json相對較大,那麼也會引起sp讀取速度的急劇下降。
JSON或者HTML格式存放在sp裡面的時候,需要轉義,這樣會帶來很多 & 這種特殊符號,sp在解析碰到這個特殊符號的時候會進行特殊的處理,引發額外的字元串拼接以及函數調用開銷。而JSON本來就是可以用來做配置文件的,你幹嘛又把它放在sp裡面呢?多此一舉。下面我寫個demo驗證一下。
下面這個sp是某個app的換膚配置:
我們先用sp進行讀取,然後用直接把它丟json文件,直接讀取並且解析;json使用的代碼如下:
public int getValueByJson(Context context, String key) { File jsonFile = new File(context.getFilesDir().getParent() + File.separator + SP_DIR_NAME, "skin_beta2.json"); FileInputStream fis = null; ByteArrayOutputStream bao = new ByteArrayOutputStream(); try { fis = new FileInputStream(jsonFile); FileChannel channel = fis.getChannel(); ByteBuffer buffer = ByteBuffer.allocate(1 << 13); // 8K int i1; while ((i1 = channel.read(buffer)) != -1) { buffer.flip(); bao.write(buffer.array(), 0, i1); buffer.clear(); } String content = bao.toString(); JSONObject jsonObject = new JSONObject(content); return jsonObject.getInt(key); } catch (IOException e) { e.printStackTrace(); } catch (JSONException e) { throw new RuntimeException("not a json file"); } finally { close(fis); close(bao); } return 0;}
然後我的測試結果是:直接解析JSON比在xml裡面要快一倍!在小米1S上結果如下:
時間jsonspMi 1S8038Nexus5X6.53.5這個JSON的讀取還沒有做任何的優化,提升潛力巨大!因此,如果你需要用JSON做配置,請不要把它存放在sp裡面!!
多次edit多次apply
我見過這樣的使用代碼:
SharedPreferences sp = getSharedPreferences("test", MODE_PRIVATE);sp.edit().putString("test1", "sss").apply();sp.edit().putString("test2", "sss").apply();sp.edit().putString("test3", "sss").apply();sp.edit().putString("test4", "sss").apply();
每次edit都會創建一個Editor對象,額外佔用內存;當然多創建幾個對象也影響不了多少;但是,多次apply也會卡界面你造嗎?
有童鞋會說,apply不是在別的線程些磁碟的嗎,怎麼可能卡界面?我帶你仔細看一下源碼。
public void apply() { final MemoryCommitResult mcr = commitToMemory(); final Runnable awaitCommit = new Runnable() { public void run() { try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException ignored) { } } }; QueuedWork.add(awaitCommit); Runnable postWriteRunnable = new Runnable() { public void run() { awaitCommit.run(); QueuedWork.remove(awaitCommit); } }; SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); notifyListeners(mcr);}
注意兩點,第一,把一個帶有await的runnable添加進了QueueWork類的一個隊列;第二,把這個寫入任務通過enqueueDiskWrite丟給了一個只有單個線程的線程池執行。
到這裡一切都OK,在子線程裡面寫入不會卡UI。但是,你去ActivityThread類的handleStopActivity里看一看:
private void handleStopActivity(IBinder token, boolean show, int configChanges, int seq) { // 省略無關。。 // Make sure any pending writes are now committed. if (!r.isPreHoneycomb()) { QueuedWork.waitToFinish(); } // 省略無關。。}
waitToFinish?? 又要等?源碼如下:
public static void waitToFinish() { Runnable toFinish; while ((toFinish = sPendingWorkFinishers.poll()) != null) { toFinish.run(); }}
還記得這個toFinish的Runnable是啥嗎?就是上面那個awaitCommit它裡面就一句話,等待寫入線程!!如果在Activity Stop的時候,已經寫入完畢了,那麼萬事大吉,不會有任何等待,這個函數會立馬返回。但是,如果你使用了太多次的apply,那麼意味著寫入隊列會有很多寫入任務,而那裡就只有一個線程在寫。當App規模很大的時候,這種情況簡直就太常見了!
因此,雖然apply是在子線程執行的,但是請不要無節制地apply;commit我就不多說了吧?直接在當前線程寫入,如果你在主線程干這個,小心挨揍。
用來跨進程
還有童鞋發現sp有一個貌似可以提供「跨進程」功能的FLAG——MODE_MULTI_PROCESS,我們看看這個FLAG的文檔:
@deprecated MODE_MULTI_PROCESS does not work reliably in
some versions of Android, and furthermore does not provide any mechanism for reconciling concurrent modifications across processes. Applications should not attempt to use it. Instead, they should use an explicit cross-process data management approach such as {@link android.content.ContentProvider ContentProvider}.
文檔也說了,這玩意在某些Android版本上不可靠,並且未來也不會提供任何支持,要是用跨進程數據傳輸需要使用類似ContentProvider的東西。而且,SharedPreference的文檔也特別說明:
Note: This class does not support use across multiple processes.
那麼我們姑且看一看,設置了這個Flag到底幹了啥;在SharedPreferenceImpl裡面,沒有發現任何對這個Flag的使用;然後我們去ContextImpl類裡面找找getSharedPreference的時候做了什麼:
@Overridepublic SharedPreferences getSharedPreferences(File file, int mode) { checkMode(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; } } 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;}
這個flag保證了啥?保證了在API 11以前的系統上,如果sp已經被讀取進內存,再次獲取這個sp的時候,如果有這個flag,會重新讀一遍文件,僅此而已!所以,如果仰仗這個Flag做跨進程存取,簡直就是丟人現眼。
小結
總價一下,sp是一種輕量級的存儲方式,使用方便,但是也有它適用的場景。要優雅滴使用sp,要注意以下幾點:
- 不要存放大的key和value!我就不重複三遍了,會引起界面卡、頻繁GC、佔用內存等等,好自為之!
- 毫不相關的配置項就不要丟在一起了!文件越大讀取越慢,不知不覺就被豬隊友給坑了;藍後,放進defalut的那個簡直就是愚蠢行為!
- 讀取頻繁的key和不易變動的key盡量不要放在一起,影響速度。(如果整個文件很小,那麼忽略吧,為了這點性能添加維護成本得不償失)
- 不要亂edit和apply,盡量批量修改一次提交!
- 盡量不要存放JSON和HTML,這種場景請直接使用json!
- 不要指望用這貨進行跨進程通信!!!
推薦閱讀:
※5天2億活躍用戶,2017QQ「LBS+AR」天降紅包活動後台揭密
※10大主流壓力/負載/性能測試工具推薦
※論壇防灌水設置如何測試?
※LR使用說明書
※哪款網站壓力測試工具值得推薦?