前言
同學(xué)猎莲,聽(tīng)說(shuō)SharedPreference你玩的很6,不就是存儲(chǔ)鍵值對(duì)嘛技即,工具類就可以搞定著洼。那下面這些問(wèn)題,你都回答的上來(lái)嗎而叼?
目錄
1身笤、SharedPreference有哪些隱患或風(fēng)險(xiǎn)?
2葵陵、為什么SharedPreference會(huì)造成卡頓甚至ANR液荸?
3、如何解決sp造成的界面卡頓脱篙、掉幀問(wèn)題娇钱?
4、commit和apply有什么區(qū)別绊困?
5忍弛、apply就不會(huì)讓主線程卡頓?
6考抄、SharedPreference如何跨進(jìn)程通信?
還沒(méi)有看過(guò)源碼的同學(xué)蔗彤,強(qiáng)烈推薦先看一遍川梅,不然會(huì)有各種不適癥狀疯兼。
Android SharedPreferences源碼都不會(huì),怎么通過(guò)面試贫途?
SharedPreference有哪些隱患或風(fēng)險(xiǎn)吧彪?
卡頓、丟幀丢早、甚至ANR姨裸、占用內(nèi)存過(guò)高。
為什么SharedPreference會(huì)造成卡頓甚至ANR怨酝?
- 第一次從SharedPreference獲取值的時(shí)候傀缩,可能阻塞主線程,造成卡頓/丟幀农猬。
看如下代碼赡艰,我第一次從sp取數(shù)據(jù)竟然花費(fèi)了11ms
。這還是我的數(shù)據(jù)很少的情況下斤葱,很多時(shí)候慷垮,一個(gè)迭代了很多版本的項(xiàng)目存放的數(shù)據(jù)會(huì)遠(yuǎn)比我的要大,耗時(shí)也會(huì)更長(zhǎng)揍堕。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
var startTime = System.currentTimeMillis()
val sp: SharedPreferences = getSharedPreferences("sp", Context.MODE_PRIVATE)
val value: String? = sp.getString("key", "")
Log.e(TAG, "func1 : ${System.currentTimeMillis() - startTime}")
}
有人會(huì)說(shuō)SharedPreferences 的加載是不是在子線程嗎料身,為什么還會(huì)阻塞主線程呢?這個(gè)問(wèn)題衩茸,我們要從源碼中尋找答案芹血。
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
//阻塞等待加載、解析xml文件完成
awaitLoadedLocked();
//從內(nèi)存獲取
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
從內(nèi)存獲取數(shù)據(jù)之前递瑰,還調(diào)用了 awaitLoadedLocked()祟牲,下面來(lái)看看這個(gè)方法。
private void awaitLoadedLocked() {
if (!mLoaded) {
// Raise an explicit StrictMode onReadFromDisk for this
// thread, since the real read will be in a different
// thread and otherwise ignored by StrictMode.
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
//關(guān)鍵點(diǎn)抖部,object對(duì)象的wait()來(lái)阻塞等待
mLock.wait();
} catch (InterruptedException unused) {
}
}
}
awaitLoadedLocked()會(huì)循環(huán)等待说贝,直到mLoaded為true,那什么時(shí)候mLoaded為true呢慎颗?答案是從磁盤(pán)加載乡恕、解析xml完成之后,具體是在SharedPreferencesImpl#loadFromDisk()
方法內(nèi)俯萎,這里不展開(kāi)了傲宜,可以去上一篇源碼分析文章看。
小結(jié)夫啊,第一次獲取數(shù)據(jù)的時(shí)候會(huì)阻塞主線程函卒,原因是主線程會(huì)等待從文件加載sp完成,這是一個(gè)耗時(shí)操作撇眯,尤其是xml中數(shù)據(jù)比較大的時(shí)候更明顯报嵌;注意:只有第一次才會(huì)虱咧,后面不會(huì),因?yàn)榧虞d文件成功后會(huì)在內(nèi)存緩存數(shù)據(jù)锚国,下次就不需要等待了腕巡。
怎么解決?盡可能早的去完成sp對(duì)象的初始化血筑,通常在Application是最合適的绘沉。
多次commit、apply
我見(jiàn)過(guò)很多這樣的代碼,每次寫(xiě)入數(shù)據(jù)都會(huì)創(chuàng)建一個(gè)Editor對(duì)象豺总,調(diào)用一次commit/apply车伞。
val sp: SharedPreferences = getSharedPreferences("sp", Context.MODE_PRIVATE)
sp.edit().putString("key0", "11").apply()
sp.edit().putString("key1", "11").apply()
sp.edit().putString("key2", "11").apply()
創(chuàng)建Editor對(duì)象和put方法并不怎么耗時(shí),但是多次commit()/apply()有多耗時(shí)园欣,您心里沒(méi)數(shù)嗎帖世?不信就來(lái)看看下面這組數(shù)據(jù)。
存儲(chǔ)方式 | 數(shù)據(jù)量 | 耗時(shí)(ms) |
---|---|---|
多次commit | 20 | 116 |
多次apply | 20 | 5 |
一次性commit | 20 | 6 |
一次性apply | 20 | 1 |
所以沸枯,請(qǐng)把你的代碼改成這樣日矫。另外在不需要返回值的時(shí)候,請(qǐng)你使用apply()
绑榴,官方也是這樣推薦的哪轿。
val sp: SharedPreferences = getSharedPreferences("sp", Context.MODE_PRIVATE)
var editor = sp.edit()
editor.putString("key0", "11")
.putString("key1", "11")
.putString("key2", "11")
.apply()
我們都知道commit()是在主線程寫(xiě)入文件的,很可能會(huì)卡頓甚至ANR翔怎。有人會(huì)說(shuō)窃诉,用apply()不就可以了嗎?
模擬面試
面試官:sp的apply()會(huì)造成卡頓嗎赤套?
小A:不會(huì)卡頓飘痛,commit才會(huì)卡頓,因?yàn)閍pply是在子線程寫(xiě)入文件的容握⌒觯【得意??】
面試官:你確定不會(huì)卡?
小A:我確定剔氏。
面試官:其實(shí)也可能會(huì)卡的塑猖。吧啦吧啦。谈跛。羊苟。「泻叮【得意??】
小A:大佬蜡励,原來(lái)還可以這樣,佛了。
下面就把面試官解釋的這一段補(bǔ)充完整巍虫。
先來(lái)看一下apply方法彭则,注釋有點(diǎn)多,可以幫到喜歡追求細(xì)節(jié)的朋友
public void apply() {
//修改內(nèi)存緩存mMap
final MemoryCommitResult mcr = commitToMemory();
//等待寫(xiě)入文件完成的任務(wù)
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try {
//阻塞等待寫(xiě)入文件完成,否則阻塞在這
//利用CountDownLatch來(lái)等待任務(wù)的完成
//后面執(zhí)行enqueueDiskWrite寫(xiě)入文件成功后會(huì)把writtenToDiskLatch多線程計(jì)數(shù)器減1占遥, 這樣的話下面的阻塞代碼就可以通過(guò)了.
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
//QueuedWork是用來(lái)確保SharedPrefenced的寫(xiě)操作在Activity 銷毀前執(zhí)行完的一個(gè)全局隊(duì)列.
//QueuedWork里面的隊(duì)列是通過(guò)LinkedList實(shí)現(xiàn)的,LinkedList不僅可以做鏈表输瓜,也可以做隊(duì)列
//添加到全局的工作隊(duì)列中
QueuedWork.addFinisher(awaitCommit);
//這個(gè)任務(wù)是等待磁盤(pán)寫(xiě)入完成瓦胎,然后從隊(duì)列中移除任務(wù)
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
//執(zhí)行阻塞任務(wù)
awaitCommit.run();
//阻塞完成之后,從隊(duì)列中移除任務(wù)
QueuedWork.removeFinisher(awaitCommit);
}
};
//異步執(zhí)行磁盤(pán)文件寫(xiě)入尤揣,注意這里和commit不同的是postWriteRunnable不為空
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
關(guān)鍵點(diǎn)1:把一個(gè)帶有await的runnable添加進(jìn)了QueueWork類的一個(gè)隊(duì)列,注意一下這個(gè)sFinishers
搔啊,等會(huì)兒會(huì)用到
//把任務(wù)添加到全局的工作隊(duì)列中
QueuedWork.addFinisher(awaitCommit);
public class QueuedWork {
/** Finishers {@link #addFinisher added} and not yet {@link #removeFinisher removed} */
private static final LinkedList<Runnable> sFinishers = new LinkedList<>();
public static void addFinisher(Runnable finisher) {
synchronized (sLock) {
sFinishers.add(finisher);
}
}
}
關(guān)鍵點(diǎn)2:把寫(xiě)入文件的任務(wù)放入一個(gè)隊(duì)列中,在QueuedWork內(nèi)部會(huì)通過(guò)HandlerThread串行的執(zhí)行北戏。
//apply異步寫(xiě)入文件
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
到這里负芋,看上去還沒(méi)有問(wèn)題,在子線程寫(xiě)文件并不會(huì)造成UI線程卡頓嗜愈,但是我們來(lái)看一下ActivityThread的handleStopActivity
方法旧蛾。
public void handleStopActivity(IBinder token, boolean show, int configChanges,
PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
...省略無(wú)關(guān)代碼
// Make sure any pending writes are now committed.
if (!r.isPreHoneycomb()) {
QueuedWork.waitToFinish();
}
}
//如果sdk版本<11返回false
private boolean isPreHoneycomb() {
return activity != null && activity.getApplicationInfo().targetSdkVersion
< android.os.Build.VERSION_CODES.HONEYCOMB;
}
注意,在onPause
會(huì)調(diào)用handleStopActivity()方法蠕嫁,而且?guī)缀醵紩?huì)執(zhí)行QueuedWork.waitToFinish();
方法锨天。waitToFinish?看上去像是等待完成剃毒?
public static void waitToFinish() {
while (true) {
Runnable finisher;
synchronized (sLock) {
//從隊(duì)列取任務(wù)
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}
finisher.run();
}
}
這個(gè)方法很簡(jiǎn)單病袄,循環(huán)地從sFinishers這個(gè)隊(duì)列中取任務(wù)執(zhí)行,直到任務(wù)為空赘阀。這個(gè)任務(wù)就是之前apply中的awaitCommit益缠,它是用來(lái)等待寫(xiě)入文件的線程執(zhí)行完畢的。現(xiàn)在試想一下基公,在onPause之后幅慌,如果因?yàn)槟愣啻问褂昧薬pply,那就意味著寫(xiě)入任務(wù)會(huì)在這里排隊(duì)酌媒,但是寫(xiě)入文件那里只有一個(gè)HandlerThread在串行的執(zhí)行欠痴,那是不是就卡頓了?
給出幾條建議:
- 第1:不要多次apply秒咨,請(qǐng)合并為1次事物提交喇辽,因?yàn)镮/O真的很耗時(shí);
- 第2:請(qǐng)你不要在sp存放太大的數(shù)據(jù)雨席,比如json之類菩咨,因?yàn)槲募蟪跏蓟瘯?huì)很耗時(shí),而且文件內(nèi)容會(huì)一直緩存在內(nèi)存中,得不到釋放抽米;
- 第3:如果你的apply只是存儲(chǔ)了輕量級(jí)的數(shù)據(jù)特占,比如true、"abc"這樣的內(nèi)容云茸,請(qǐng)大膽的使用是目,沒(méi)有什么性能影響;
- 第4:如果優(yōu)化了apply還出現(xiàn)卡頓标捺,就用commit吧懊纳,但是需要自己進(jìn)行異步處理,至于用Thread還是線程池或者其它看你自己業(yè)務(wù)亡容。
如何解決sp造成的界面卡頓嗤疯、掉幀問(wèn)題?
其實(shí)上面的分析已經(jīng)給出答案了闺兢,這里再總結(jié)一下茂缚。
- 1.初始化sp放在application;
- 2.不要頻繁的commit/apply,盡量使用一次事物提交屋谭;
- 3.優(yōu)先選擇用apply而不是commit脚囊,因?yàn)閏ommit會(huì)卡UI;
- 4.sp是輕量級(jí)的存儲(chǔ)工具戴而,所以請(qǐng)你不要存放太大的數(shù)據(jù)凑术,不要存json等;
- 5.單個(gè)sp文件不要太大所意,如果數(shù)據(jù)量很大淮逊,請(qǐng)把關(guān)聯(lián)性比較大的,高頻操作的放在單獨(dú)的sp文件
commit和apply有什么區(qū)別扶踊?
- commit()是同步且有返回值的泄鹏;apply()方法是異步?jīng)]有返回值的;
- commit()在主線程寫(xiě)入文件秧耗,會(huì)造成UI卡頓备籽;apply()在子線程寫(xiě)入文件,也有可能卡UI分井;
SharedPreference如何跨進(jìn)程通信车猬?
有人寄希望于在初始化sp的時(shí)候,設(shè)置flag為MODE_MULTI_PROCESS
來(lái)跨進(jìn)程通信尺锚,但是很遺憾珠闰,這種方式已經(jīng)被廢棄。
getSharedPreferences("sp", Context.MODE_MULTI_PROCESS)
* @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}.
*/
@Deprecated
public static final int MODE_MULTI_PROCESS = 0x0004;
如果要跨進(jìn)程通信瘫辩,需要在sp外面包裹一層ContentProvider伏嗜,當(dāng)然用mmkv性能上更佳坛悉。
感謝以下作者
Android SharedPreference源碼閱讀
SharedPreferences靈魂拷問(wèn)之原理
http://www.reibang.com/p/19f261f436ae
Android SharedPreferences.apply() 問(wèn)題