不論從事安卓應用開發,仍是安卓系統研發,應該都遇到應用無響應(ANR,Application Not Responding)問題,當應用程序一段時間沒法及時響應,則會彈出ANR對話框,讓用戶選擇繼續等待,仍是強制關閉。android
絕大多數人對ANR的瞭解僅停留在主線程耗時或CPU繁忙會致使ANR。面試過無數的候選人,幾乎沒有人能真正從系統級去梳理清晰ANR的前因後果,好比有哪些路徑會引起ANR? 有沒有可能主線程不耗時也出現ANR?如何更好的調試ANR?ios
若是沒有深刻研究過Android Framework的源代碼,是難以造成對ANR有一個全面、正確的理解。研究系統源碼以及工做實踐後提煉而來,以圖文並茂的方式跟你們講解,相信定能幫忙你們加深對ANR的理解。git
對於知識學習的過程,要知其然知其因此然,才能作到庖丁解牛般遊刃有餘。要深刻理解ANR,就須要從根上去找尋答案,那就是ANR是如何觸發的?面試
ANR是一套監控Android應用響應是否及時的機制,能夠把發生ANR比做是引爆炸彈,那麼整個流程包含三部分組成:算法
常見的ANR有service、broadcast、provider以及input,更多細節詳見理解Android ANR的觸發原理,gityuan.com/2016/07/02/… ,接下來本文以圖文形式分別講解。shell
下面來看看埋炸彈與拆炸彈在整個服務啓動(startService)過程所處的環節。網絡
圖解1:app
更多細節詳見startService啓動過程分析,gityuan.com/2016/03/06/…異步
broadcast跟service超時機制大抵相同,對於靜態註冊的廣播在超時檢測過程須要檢測SP,以下圖所示。socket
圖解2:
(說明:SP從8.0開始採用名叫「queued-work-looper」的handler線程,在老版本採用newSingleThreadExecutor建立的單線程的線程池)
若是是動態廣播,或者靜態廣播沒有正在執行持久化操做的SP任務,則不須要通過「queued-work-looper」線程中轉,而是直接向中控系統彙報,流程更爲簡單,以下圖所示:
可見,只有XML靜態註冊的廣播超時檢測過程會考慮是否有SP還沒有完成,動態廣播並不受其影響。SP的apply將修改的數據項更新到內存,而後再異步同步數據到磁盤文件,所以不少地方會推薦在主線程調用採用apply方式,避免阻塞主線程,但靜態廣播超時檢測過程須要SP所有持久化到磁盤,若是過分使用apply會增大應用ANR的機率,更多細節詳見http://gityuan.com/2017/06/18/SharedPreferences
Google這樣設計的初衷是針對靜態廣播的場景下,保障進程被殺以前必定能完成SP的數據持久化。由於在向中控系統彙報廣播接收者工做執行完成前,該進程的優先級爲Foreground級別,高優先級下進程不但不會被殺,並且能分配到更多的CPU時間片,加速完成SP持久化。
更多細節詳見Android Broadcast廣播機制分析,gityuan.com/2016/06/04/…
provider的超時是在provider進程首次啓動的時候纔會檢測,當provider進程已啓動的場景,再次請求provider並不會觸發provider超時。
圖解3:
更多細節詳見理解ContentProvider原理,gityuan.com/2016/07/30/…
input的超時檢測機制跟service、broadcast、provider大相徑庭,爲了更好的理解input過程先來介紹兩個重要線程的相關工做:
input的超時機制並不是時間到了必定就會爆炸,而是處理後續上報事件的過程纔會去檢測是否該爆炸,因此更相信是掃雷的過程,具體以下圖所示。
圖解4:
input超時機制爲何是掃雷,而非定時爆炸呢?是因爲對於input來講即使某次事件執行時間超過timeout時長,只要用戶後續在沒有再生成輸入事件,則不會觸發ANR。 這裏的掃雷是指當前輸入系統中正在處理着某個耗時事件的前提下,後續的每一次input事件都會檢測前一個正在處理的事件是否超時(進入掃雷狀態),檢測當前的時間距離上次輸入事件分發時間點是否超過timeout時長。若是前一個輸入事件,則會重置ANR的timeout,從而不會爆炸。
更多細節詳見Input系統-ANR原理分析,gityuan.com/2017/01/01/…
不一樣組件的超時閾值各有不一樣,關於service、broadcast、contentprovider以及input的超時閾值以下表:
系統對前臺服務啓動的超時爲20s,然後臺服務超時爲200s,那麼系統是如何區別前臺仍是後臺服務呢?來看看ActiveServices的核心邏輯:
ComponentName startServiceLocked(...) {
final boolean callerFg;
if (caller != null) {
final ProcessRecord callerApp = mAm.getRecordForAppLocked(caller);
callerFg = callerApp.setSchedGroup != ProcessList.SCHED_GROUP_BACKGROUND;
} else {
callerFg = true;
}
...
ComponentName cmp = startServiceInnerLocked(smap, service, r, callerFg, addToStarting);
return cmp;
}
複製代碼
在startService過程根據發起方進程callerApp所屬的進程調度組來決定被啓動的服務是屬於前臺仍是後臺。當發起方進程不等於ProcessList.SCHED_GROUP_BACKGROUND(後臺進程組)則認爲是前臺服務,不然爲後臺服務,並標記在ServiceRecord的成員變量createdFromFg。
什麼進程屬於SCHED_GROUP_BACKGROUND調度組呢?進程調度組大致可分爲TOP、前臺、後臺,進程優先級(Adj)和進程調度組(SCHED_GROUP)算法較爲複雜,其對應關係可粗略理解爲Adj等於0的進程屬於Top進程組,Adj等於100或者200的進程屬於前臺進程組,Adj大於200的進程屬於後臺進程組。關於Adj的含義見下表,簡單來講就是Adj>200的進程對用戶來講基本是無感知,主要是作一些後臺工做,故後臺服務擁有更長的超時閾值,同時後臺服務屬於後臺進程調度組,相比前臺服務屬於前臺進程調度組,分配更少的CPU時間片。
關於細節詳看法讀Android進程優先級ADJ算法,gityuan.com/2018/05/19/…
前臺服務準確來講,是指由處於前臺進程調度組的進程發起的服務
。這跟常說的fg-service服務有所不一樣,fg-service是指掛有前臺通知的服務。
前臺廣播超時爲10s,後臺廣播超時爲60s,那麼如何區分前臺和後臺廣播呢?來看看AMS的核心邏輯:
BroadcastQueue broadcastQueueForIntent(Intent intent) {
final boolean isFg = (intent.getFlags() & Intent.FLAG_RECEIVER_FOREGROUND) != 0;
return (isFg) ? mFgBroadcastQueue : mBgBroadcastQueue;
}
mFgBroadcastQueue = new BroadcastQueue(this, mHandler,
"foreground", BROADCAST_FG_TIMEOUT, false);
mBgBroadcastQueue = new BroadcastQueue(this, mHandler,
"background", BROADCAST_BG_TIMEOUT, true);
複製代碼
根據發送廣播sendBroadcast(Intent intent)中的intent的flags是否包含FLAG_RECEIVER_FOREGROUND來決定把該廣播是放入前臺廣播隊列或者後臺廣播隊列,前臺廣播隊列的超時爲10s,後臺廣播隊列的超時爲60s,默認狀況下廣播是放入後臺廣播隊列,除非指明加上FLAG_RECEIVER_FOREGROUND標識。
後臺廣播比前臺廣播擁有更長的超時閾值,同時在廣播分發過程遇到後臺service的啓動(mDelayBehindServices)會延遲分發廣播,等待service的完成,由於等待service而致使的廣播ANR會被忽略掉;後臺廣播屬於後臺進程調度組,而前臺廣播屬於前臺進程調度組。簡而言之,後臺廣播更不容易發生ANR,同時執行的速度也會更慢。
另外,只有串行處理的廣播纔有超時機制,由於接收者是串行處理的,前一個receiver處理慢,會影響後一個receiver;並行廣播經過一個循環一次性向全部的receiver分發廣播事件,因此不存在彼此影響的問題,則沒有廣播超時。
前臺廣播準確來講,是指位於前臺廣播隊列的廣播
。
除了前臺服務,前臺廣播,還有前臺ANR可能會讓你雲裏霧裏的,來看看其中核心邏輯:
final void appNotResponding(...) {
...
synchronized (mService) {
isSilentANR = !showBackground && !isInterestingForBackgroundTraces(app);
...
}
...
File tracesFile = ActivityManagerService.dumpStackTraces(
true, firstPids,
(isSilentANR) ? null : processCpuTracker,
(isSilentANR) ? null : lastPids,
nativePids);
synchronized (mService) {
if (isSilentANR) {
app.kill("bg anr", true);
return;
}
...
//彈出ANR選擇的對話框
Message msg = Message.obtain();
msg.what = ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG;
msg.obj = new AppNotRespondingDialog.Data(app, activity, aboveSystem);
mService.mUiHandler.sendMessage(msg);
}
}
複製代碼
決定是前臺或者後臺ANR取決於該應用發生ANR時對用戶是否可感知,好比擁有當前前臺可見的activity的進程,或者擁有前臺通知的fg-service的進程,這些是用戶可感知的場景,發生ANR對用戶體驗影響比較大,故須要彈框讓用戶決定是否退出仍是等待,若是直接殺掉這類應用會給用戶形成莫名其妙的閃退。
後臺ANR相比前臺ANR,只抓取發生無響應進程的trace,也不會收集CPU信息,而且會在後臺直接殺掉該無響應的進程,不會彈框提示用戶。
前臺ANR準確來講,是指對用戶可感知的進程發生的ANR
。
對於service、broadcast、provider、input發生ANR後,中控系統會立刻去抓取現場的信息,用於調試分析。收集的信息包括以下:
整個ANR信息收集過程比較耗時,其中抓取進程的trace信息,每抓取一個等待200ms,可見persistent越多,等待時間越長。關於抓取trace命令,對於Java進程可經過在adb shell環境下執行kill -3 [pid]可抓取相應pid的調用棧;對於Native進程在adb shell環境下執行debuggerd -b [pid]可抓取相應pid的調用棧。對於ANR問題發生後的蛛絲馬跡(trace)在traces.txt和dropbox目錄中保存記錄。更多細節詳見理解Android ANR的信息收集過程,gityuan.com/2016/12/02/…
有了現場信息,能夠調試分析,先定位發生ANR時間點,而後查看trace信息,接着分析是否有耗時的message、binder調用,鎖的競爭,CPU資源的搶佔,以及結合具體場景的上下文來分析,調試手段就須要針對前面說到的message、binder、鎖等資源從系統角度細化更多debug信息,這裏再也不展開,後續再以ANR案例來說解。
做爲應用開發者應讓主線程儘可能只作UI相關的操做,避免耗時操做,好比過分複雜的UI繪製,網絡操做,文件IO操做;避免主線程跟工做線程發生鎖的競爭,減小系統耗時binder的調用,謹慎使用sharePreference,注意主線程執行provider query操做。簡而言之,儘量減小主線程的負載,讓其空閒待命,以期可隨時響應用戶的操做。
最後,來回答文章開頭的提問,有哪些路徑會引起ANR? 答應是從埋下定時炸彈到拆炸彈之間的任何一個或多個路徑執行慢都會致使ANR(以service爲例),能夠是service的生命週期的回調方法(好比onStartCommand)執行慢,能夠是主線程的消息隊列存在其餘耗時消息讓service回調方法遲遲得不到執行,能夠是SP操做執行慢,能夠是system_server進程的binder線程繁忙而致使沒有及時收到拆炸彈的指令。另外ActivityManager線程也可能阻塞,出現的現象就是前臺服務執行時間有可能超過10s,但並不會出現ANR。
發生ANR時從trace來看主線程卻處於空閒狀態或者停留在非耗時代碼的緣由有哪些?能夠是抓取trace過於耗時而錯過現場,能夠是主線程消息隊列堆積大量消息而最後抓取快照一刻只是瞬時狀態,能夠是廣播的「queued-work-looper」一直在處理SP操做。
本文的知識源自對Android系統源碼的研究以及工做實踐中提煉而來,Android達摩院獨家武功祕籍分享給你們,但願能升你們對提對ANR的理解。