那天我突然看到有人說(shuō)使用SharedPreferences會(huì)出現(xiàn)ANR豌鸡,要知道ANR可是個(gè)大問(wèn)題啊氢卡,于是我就想看看SharedPreferences是如何實(shí)現(xiàn)的,然后記錄于此,還請(qǐng)各位指點(diǎn)建邓!
我們都知道,我們是通過(guò)Context來(lái)獲取SharedPreferences的春缕,于是盗胀,我們就在Context中看到這么一個(gè)抽象方法,比較Context本身就是個(gè)抽象類(lèi)嘛锄贼!
public abstract SharedPreferences getSharedPreferences(String name, @PreferencesMode int mode);
然后票灰,很顯然,我們得找到Context的具體實(shí)現(xiàn)類(lèi)宅荤,才能知道getSharedPreferences這個(gè)方法里面的邏輯屑迂,我們很快就注意到ContextWrapper,而且我們的Activity和Application都繼承于ContextWrapper冯键,然后我們就找到這樣的一個(gè)實(shí)現(xiàn)方法惹盼,如下:
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
return mBase.getSharedPreferences(name, mode);
}
其中mBase是申明的一個(gè)Context成員變量,而我們想看getSharedPreferences的具體實(shí)現(xiàn)惫确,找到mBase就是關(guān)鍵逻锐,ContextWrapper中相關(guān)代碼如下:
Context mBase;
public ContextWrapper(Context base) {
mBase = base;
}
protected void attachBaseContext(Context base) {
if (mBase != null) {
throw new IllegalStateException("Base context already set");
}
mBase = base;
}
然后我們注意到mBase有兩種初始化方法,其中attachBaseContext值得我們注意雕薪,畢竟我們記得在Activity的源碼中就有attachBaseContext方法昧诱,如下:
@Override
protected void attachBaseContext(Context newBase) {
super.attachBaseContext(newBase);
if (newBase != null) {
newBase.setAutofillClient(this);
}
}
其中 super.attachBaseContext(newBase)這個(gè)調(diào)用的就是ContextWrapper中的attachBaseContext方法。同樣的所袁,Application中的attach方法中也有調(diào)用到該方法盏档。
我們繼續(xù)查看,發(fā)現(xiàn)Activity的attachBaseContext是由attach方法調(diào)用的燥爷,我們來(lái)看看這個(gè)方法:
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
attachBaseContext(context);
......
}
我們很自然地注意到第二個(gè)參數(shù)ActivityThread蜈亩,畢竟總所周知,ActivityThread才是應(yīng)用真正的入口前翎,所以稚配,我們的關(guān)注點(diǎn)應(yīng)該轉(zhuǎn)到ActivityThread上來(lái),我們來(lái)看看其入口的代碼:
public static void main(String[] args) {
.......
Looper.prepareMainLooper();
.......
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}
// End of event ActivityThreadMain.
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
Looper.loop();
throw new RuntimeException("Main thread loop unexpectedly exited");
}
我們摘取了關(guān)鍵代碼港华,很顯然道川,這就是android的消息分發(fā)機(jī)制,其中我們用的是主線(xiàn)程的Looper立宜,所以其Handler也默認(rèn)綁定到主線(xiàn)程中冒萄,于是我們的四大組件默認(rèn)都運(yùn)行在主線(xiàn)程中的,這一點(diǎn)必須明確橙数!
另外尊流,注意到最后拋出的異常沒(méi),說(shuō)明Looper死循環(huán)必須貫穿APP整個(gè)生命周期灯帮,除非APP退出了崖技,否則必須一直進(jìn)行Looper死循環(huán)逻住,否則就會(huì)拋出異常。
好了迎献,上面這些屬于順帶一提的瞎访,說(shuō)這個(gè)入口主要是想說(shuō)下面這兩行代碼:
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
這里會(huì)進(jìn)行什么操作呢?attach傳false忿晕,說(shuō)明不是系統(tǒng)應(yīng)用装诡,會(huì)直接綁定到AMS(ActivityManagerService)银受,如下attach中的關(guān)鍵代碼:
final IActivityManager mgr = ActivityManager.getService();
try {
mgr.attachApplication(mAppThread, startSeq);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
正如大家所知道的践盼,其實(shí)ActivityThread擁有與AMS交互并且管理Activity和Service等組件的重要作用。
至于ActivityThread與AMS間的交互宾巍,其實(shí)都是進(jìn)行著一個(gè)個(gè)IPC交互咕幻,你看到上面代碼中的RemoteException沒(méi)?在這里我們就不對(duì)IPC調(diào)用作過(guò)多的說(shuō)明和深究了顶霞,我們只需知道一點(diǎn):其實(shí)Activity的每個(gè)生命周期調(diào)用肄程,其實(shí)是由ActivityThread與AMS進(jìn)行IPC交互來(lái)完成的。
我們明白了上述過(guò)程后选浑,發(fā)現(xiàn)了ActivityThread中有很多handle開(kāi)頭的方法蓝厌,如下圖所示:
沒(méi)錯(cuò),這些就是IPC后通過(guò)消息分發(fā)機(jī)制調(diào)用的方法古徒,我們重點(diǎn)關(guān)注的是Activity的啟動(dòng)拓提,如上圖紅圈中的方法:
/**
* Extended implementation of activity launch. Used when server requests a launch or relaunch.
*/
@Override
public Activity handleLaunchActivity(ActivityClientRecord r,
PendingTransactionActions pendingActions, Intent customIntent) {
.......
final Activity a = performLaunchActivity(r, customIntent);
.......
}
handleLaunchActivity中我們注意到performLaunchActivity這個(gè)方法,關(guān)鍵代碼如下:
/** Core implementation of activity launch. */
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
.......
ContextImpl appContext = createBaseContextForActivity(r);
.......
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback);
.......
}
終于到這里來(lái)了隧膘,看到?jīng)]代态?在performLaunchActivity方法里,我們創(chuàng)建了ContextImpl疹吃,然后將其傳給了activity蹦疑,于是,我們苦苦追尋的實(shí)現(xiàn)類(lèi)就是ContextImpl萨驶,當(dāng)然歉摧,光憑名字我就知道它是Context的真正實(shí)現(xiàn)類(lèi)了,只是腔呜,我們還是必須要這樣有憑有據(jù)判莉、明明白白滴追蹤一下代碼!
當(dāng)然育谬,我們回顧一下上面ContextWrapper券盅、ContextImpl和Context三者的關(guān)系,從設(shè)計(jì)模式的角度上來(lái)說(shuō)膛檀,這里應(yīng)用了代理模式锰镀。
其中娘侍,Context為抽象主題類(lèi),ContextImpl為真實(shí)主題類(lèi)泳炉,ContextWrapper為代理類(lèi)憾筏,ContextImpl和ContextWrapper都繼承于Context,然后ContextWrapper持有ContextImpl的引用花鹅,完成了對(duì)ContextImpl的代理操作氧腰。
接下來(lái)我們重點(diǎn)就放在ContextImpl來(lái)了,我們可以看到getSharedPreferences的真正實(shí)現(xiàn)刨肃,如下:
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
// At least one application in the world actually passes in a null
// name. This happened to work because when we generated the file name
// we would stringify it to "null.xml". Nice.
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
我們可以看到古拴,最終會(huì)調(diào)用的是getSharedPreferences(File file, int mode),注意到其中創(chuàng)建文件的方法:
@Override
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
private File getPreferencesDir() {
synchronized (mSync) {
if (mPreferencesDir == null) {
mPreferencesDir = new File(getDataDir(), "shared_prefs");
}
return ensurePrivateDirExists(mPreferencesDir);
}
}
private File makeFilename(File base, String name) {
if (name.indexOf(File.separatorChar) < 0) {
return new File(base, name);
}
throw new IllegalArgumentException(
"File " + name + " contains a path separator");
}
從上面這3個(gè)方法真友,我們可以知道:
1)SharedPreferences其實(shí)存儲(chǔ)為一個(gè)xml文件
2)SharedPreferences存儲(chǔ)的位置在這個(gè)目錄下:/data/data/你APP包名/shared_prefs/
3)創(chuàng)建SharedPreferences的名字中不能含有“/”這樣的分隔符黄痪,否則會(huì)報(bào)錯(cuò)
接著,我們來(lái)看看最終創(chuàng)建的方法:getSharedPreferences(File file, int mode):
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
checkMode(mode);
if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
if (isCredentialProtectedStorage()
&& !getSystemService(UserManager.class)
.isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
throw new IllegalStateException("SharedPreferences in credential encrypted "
+ "storage are not available until after user is unlocked");
}
}
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;
}
代碼也很簡(jiǎn)單盔然,我們最終返回的是SharedPreferencesImpl對(duì)象桅打,它是SharedPreferences的實(shí)現(xiàn)類(lèi),然后通過(guò)一個(gè)ArrayMap進(jìn)行了緩存愈案,一個(gè)文件對(duì)應(yīng)一個(gè)SharedPreferencesImpl挺尾,然后再追其根源,其實(shí)就是一個(gè)文件名對(duì)應(yīng)一個(gè)SharedPreferencesImpl站绪,為什么這么說(shuō)呢遭铺?因?yàn)槲覀儫o(wú)法直接調(diào)用這個(gè)方法:getSharedPreferences(File file, int mode),源碼也以注釋說(shuō)明清楚了崇众,不信你可以去試試看O(∩_∩)O
我們回到方法的最后掂僵,意思就是當(dāng)mode為Context.MODE_MULTI_PROCESS時(shí),會(huì)執(zhí)行:sp.startReloadIfChangedUnexpectedly();
這個(gè)就是說(shuō)當(dāng)在多進(jìn)程時(shí)顷歌,每次獲取SharedPreferences都會(huì)嘗試去重新加載數(shù)據(jù)锰蓬,以防數(shù)據(jù)發(fā)生變化而不一致。這就是SharedPreferences在多進(jìn)程中保證數(shù)據(jù)正確性的方法眯漩,當(dāng)然芹扭,Context.MODE_MULTI_PROCESS這個(gè)已經(jīng)是被廢棄掉了,谷歌推薦使用ContentProivder來(lái)完成多進(jìn)程文件的共享赦抖,而不是SharedPreferences舱卡,至于原因,后面加以說(shuō)明队萤。
接下來(lái)我們看看SharedPreferencesImpl的構(gòu)造函數(shù):
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
這里嘛轮锥,注意到makeBackupFile,這個(gè)是搞了一個(gè)備份文件“.bak”要尔,以防在保存數(shù)據(jù)過(guò)程中存現(xiàn)中斷舍杜,下次進(jìn)來(lái)時(shí)可以通過(guò)備份文件進(jìn)行恢復(fù)新娜,算是一種保險(xiǎn)措施吧,當(dāng)然既绩,它也僅僅只能恢復(fù)保存進(jìn)“.bak”文件中的數(shù)據(jù)而已概龄。
我們重點(diǎn)要看的是最后一個(gè)方法:
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
原來(lái),這里直接創(chuàng)建一個(gè)線(xiàn)程饲握,將SharedPreferences文件中的數(shù)據(jù)直接加載到內(nèi)存中去私杜,這里需要說(shuō)明兩點(diǎn):
1)不用使用SharedPreferences存儲(chǔ)大量的數(shù)據(jù),不然你想啊救欧,那么大的數(shù)據(jù)直接load進(jìn)內(nèi)存衰粹,簡(jiǎn)直了···
2)這里也解釋了上面說(shuō)到的不建議使用Context.MODE_MULTI_PROCESS的原因:因?yàn)樯厦娴膕tartReloadIfChangedUnexpectedly會(huì)調(diào)用到startLoadFromDisk這個(gè)方法,這樣一來(lái)颜矿,在多進(jìn)程環(huán)境中寄猩,很多時(shí)候獲取SharedPreferences會(huì)多次load數(shù)據(jù)進(jìn)內(nèi)存嫉晶,這浪費(fèi)了內(nèi)存的緩存作用骑疆,同時(shí)讀寫(xiě)IO也會(huì)影響性能。
上面說(shuō)到的替废,將SharedPreferences文件中的數(shù)據(jù)load進(jìn)內(nèi)存箍铭,根據(jù)源碼會(huì)保存為一個(gè) Map<String, Object>鍵值對(duì)對(duì)象,然后之后的所有讀操作都從這個(gè)Map對(duì)象中讀取椎镣,這也解釋了為什么SharedPreferences在第一次讀會(huì)較慢诈火,而后面就很快了?那是因?yàn)榈谝淮巫x時(shí)需要花時(shí)間將數(shù)據(jù)load進(jìn)內(nèi)存状答,之后都從Map讀就很快了冷守。
說(shuō)完讀,我們接著說(shuō)寫(xiě)惊科,說(shuō)寫(xiě)就離不開(kāi)要說(shuō)commit 和 apply拍摇。
我們都知道一般建議使用apply,就算你用了commit 馆截,AndroidStudio也會(huì)給出這樣的提醒:
Consider using apply() instead of commit on shared preferences. Whereas commit blocks and writes its data to persistent storage immediately, apply will handle it in the background.
當(dāng)然充活,如果你使用commit然后用變量接收commit 的結(jié)果的話(huà),就沒(méi)有上面的提醒蜡娶,這也說(shuō)明了commit 和 apply的第一個(gè)不同:commit有一個(gè)boolean返回值混卵,而apply沒(méi)有。
我們直接貼出commit 和 apply兩方法的關(guān)鍵代碼:
@Override
public boolean commit() {
......
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
......
return mcr.writeToDiskResult;
}
@Override
public void apply() {
......
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);
......
}
很明顯窖张,
1)commit 和 apply都調(diào)用了commitToMemory幕随,該方法從名字就知道提交到內(nèi)存,也就說(shuō)兩個(gè)方法都先更新了內(nèi)存Map的數(shù)據(jù)宿接。
2)commit 和 apply兩個(gè)方法也都調(diào)用了enqueueDiskWrite赘淮,該方法從名字也能知道就是保存到本地磁盤(pán)的枢赔,而主要區(qū)別在于第二個(gè)參數(shù),commit中傳的是null拥知,而且源碼還給出了注釋?zhuān)簊ync write on this thread okay踏拜,意思就是在當(dāng)前線(xiàn)程同步寫(xiě)進(jìn)磁盤(pán);而apply則傳了一個(gè)Runnable對(duì)象低剔,然后使用了QueuedWork.queue方法加入了任務(wù)速梗,很顯然它屬于一個(gè)異步操作。
于是襟齿,這里就有了commit 和 apply的第二個(gè)不同:保存到本地磁盤(pán)姻锁,commit是同步、阻塞的猜欺,apply是異步位隶、非阻塞的。
上面說(shuō)到了QueuedWork.queue开皿,這里插一下QueuedWork相關(guān)的東西涧黄,它屬于android的一個(gè)內(nèi)部工具類(lèi),用于跟蹤那些未完成的或尚未結(jié)束的全局任務(wù)赋荆,我們也同樣來(lái)看一下其相關(guān)源碼:
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
private static Handler getHandler() {
synchronized (sLock) {
if (sHandler == null) {
HandlerThread handlerThread = new HandlerThread("queued-work-looper",
Process.THREAD_PRIORITY_FOREGROUND);
handlerThread.start();
sHandler = new QueuedWorkHandler(handlerThread.getLooper());
}
return sHandler;
}
}
很明顯了笋妥,內(nèi)部使用了HandlerThread,然后用Handler進(jìn)行消息的分發(fā)窄潭,再次證明了apply是異步春宣、非阻塞的。
當(dāng)然嫉你,我們上面apply方法中有這么一行代碼:
QueuedWork.addFinisher(awaitCommit);
這里再說(shuō)明一下月帝,QueuedWork有waitToFinish方法來(lái)保證addFinisher中的Runnable得以執(zhí)行,那么幽污,讓我們來(lái)看看waitToFinish都在哪里被調(diào)用的:
原來(lái)繞了一大圈嚷辅,我們又回到ActivityThread中來(lái)了,比如handlePauseActivity油挥,意思就是在activity暫停時(shí)就會(huì)調(diào)用waitToFinish潦蝇,該方法的目的就是確保之前提交的異步任務(wù)能被執(zhí)行完畢,而由于在ActivityThread中調(diào)用深寥,也就是在主線(xiàn)程中攘乒,所以,如果使用apply方法而出現(xiàn)ANR的話(huà)惋鹅,一般就是出現(xiàn)在調(diào)用waitToFinish這個(gè)過(guò)程则酝。
好啦,我們總算可以回到最開(kāi)始說(shuō)的問(wèn)題了:使用SharedPreferences會(huì)出現(xiàn)ANR,經(jīng)過(guò)上面一系列分析沽讹,我們得出了以下出現(xiàn)ANR的情況:
1)首次getXXX般卑,如果你的SharedPreferences中存儲(chǔ)了大量數(shù)據(jù),那么在首次獲取數(shù)據(jù)時(shí)爽雄,會(huì)將文件中的數(shù)據(jù)load進(jìn)內(nèi)存蝠检,我們得在主線(xiàn)程中等其load完畢后才能get,如果load時(shí)間很長(zhǎng)就有可能造成ANR
2)commit挚瘟,這個(gè)就很容易理解了叹谁,在主線(xiàn)程中進(jìn)行保存到本地磁盤(pán)的操作,該操作有可能出現(xiàn)ANR
3)apply乘盖,就如上面所分析的焰檩,在調(diào)用waitToFinish時(shí)有可能出現(xiàn)ANR
說(shuō)了這么多,我們是不是得寫(xiě)一個(gè)ANR出來(lái)岸┛颉析苫?沒(méi)問(wèn)題,請(qǐng)看如下代碼:
SharedPreferences sp= getSharedPreferences("mysp", 0);
SharedPreferences.Editor editorA = sp.edit();
for(int i=0;i<300000;i++){
editorA.putString("A" + i, "a" + i);
}
editorA.apply();
SharedPreferences.Editor editorB = sp.edit();
for(int i=0;i<300000;i++){
editorB.putString("B"+i,"b"+i);
}
editorB.commit();
簡(jiǎn)單粗暴穿扳,這樣就是一個(gè)因commit造成的ANR衩侥,同時(shí),我們接著看看讀取數(shù)據(jù)的情況纵揍,因?yàn)樯厦娴臄?shù)據(jù)量較大了顿乒,所以筆者在首次讀取時(shí)發(fā)現(xiàn)直接黑屏了好幾秒议街,倒是還沒(méi)出現(xiàn)ANR泽谨。
最后,我們針對(duì)上面3種出現(xiàn)ANR的情況給出如下的建議
1)請(qǐng)別往SharedPreferences中存入大量的數(shù)據(jù)特漩,數(shù)據(jù)量大時(shí)請(qǐng)考慮使用本地?cái)?shù)據(jù)庫(kù)
2)如果擔(dān)心commit在主線(xiàn)程保存數(shù)據(jù)會(huì)導(dǎo)致ANR吧雹,其實(shí)有一種做法就是直接新建一個(gè)子線(xiàn)程來(lái)執(zhí)行,可以考慮用一個(gè)單線(xiàn)程池來(lái)進(jìn)行封裝
3)apply的話(huà)可以考慮使用清理等待鎖涂身,據(jù)頭條app開(kāi)發(fā)團(tuán)隊(duì)的測(cè)試驗(yàn)證雄卷,效果還是很OK的,有空筆者再進(jìn)行上手驗(yàn)證蛤售,暫且收錄于此丁鹉。
參考鏈接:
https://blog.csdn.net/shifuhetudi/article/details/52089562
http://www.reibang.com/p/875d13458538