SharedPreference是Android上一種非常易用的輕量級存儲方式,由於其API及其友好,得到了很多很多開發者的青睞。但是,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(); } }
鏈接:https://zhuanlan.zhihu.com/p/22913991
來源:知乎
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
還記得這個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的時候做了什么:
@Override public 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!
- 不要指望用這貨進行跨進程通信!!!
