徹底理解安卓應用無響應機制

Gityuan發表於2019-04-15

引言

不論從事安卓應用開發,還是安卓系統研發,應該都遇到應用無響應(ANR,Application Not Responding)問題,當應用程式一段時間無法及時響應,則會彈出ANR對話方塊,讓使用者選擇繼續等待,還是強制關閉。

絕大多數人對ANR的瞭解僅停留在主執行緒耗時或CPU繁忙會導致ANR。面試過無數的候選人,幾乎沒有人能真正從系統級去梳理清晰ANR的來龍去脈,比如有哪些路徑會引發ANR? 有沒有可能主執行緒不耗時也出現ANR?如何更好的除錯ANR?

如果沒有深入研究過Android Framework的原始碼,是難以形成對ANR有一個全面、正確的理解。研究系統原始碼以及工作實踐後提煉而來,以圖文並茂的方式跟大家講解,相信定能幫忙大家加深對ANR的理解。

ANR觸發機制

對於知識學習的過程,要知其然知其所以然,才能做到庖丁解牛般遊刃有餘。要深入理解ANR,就需要從根上去找尋答案,那就是ANR是如何觸發的?

ANR是一套監控Android應用響應是否及時的機制,可以把發生ANR比作是引爆炸彈,那麼整個流程包含三部分組成:

  1. 埋定時炸彈:中控系統(system_server程式)啟動倒數計時,在規定時間內如果目標(應用程式)沒有幹完所有的活,則中控系統會定向炸燬(殺程式)目標。
  2. 拆炸彈:在規定的時間內幹完工地的所有活,並及時向中控系統報告完成,請求解除定時炸彈,則倖免於難。
  3. 引爆炸彈:中控系統立即封裝現場,抓取快照,蒐集目標執行慢的罪證(traces),便於後續的案件偵破(除錯分析),最後是炸燬目標。

常見的ANR有service、broadcast、provider以及input,更多細節詳見理解Android ANR的觸發原理,gityuan.com/2016/07/02/… ,接下來本文以圖文形式分別講解。

service超時機制

下面來看看埋炸彈與拆炸彈在整個服務啟動(startService)過程所處的環節。

service_anr

圖解1:

  1. 客戶端(App程式)向中控系統(system_server程式)發起啟動服務的請求
  2. 中控系統派出一名空閒的通訊員(binder_1執行緒)接收該請求,緊接著向元件管家(ActivityManager執行緒)傳送訊息,埋下定時炸彈
  3. 通訊員1號(binder_1)通知工地(service所在程式)的通訊員準備開始幹活
  4. 通訊員3號(binder_3)收到任務後轉交給包工頭(main主執行緒),加入包工頭的任務佇列(MessageQueue)
  5. 包工頭經過一番努力幹完活(完成service啟動的生命週期),然後等待SharedPreferences(簡稱SP)的持久化;
  6. 包工頭在SP執行完成後,立刻向中控系統彙報工作已完成
  7. 中控系統的通訊員2號(binder_2)收到包工頭的完工彙報後,立刻拆除炸彈。如果在炸彈倒數計時結束前拆除炸彈則相安無事,否則會引發爆炸(觸發ANR)

更多細節詳見startService啟動過程分析,gityuan.com/2016/03/06/…

broadcast超時機制

broadcast跟service超時機制大抵相同,對於靜態註冊的廣播在超時檢測過程需要檢測SP,如下圖所示。

broadcast_anr

圖解2:

  1. 客戶端(App程式)向中控系統(system_server程式)發起傳送廣播的請求
  2. 中控系統派出一名空閒的通訊員(binder_1)接收該請求轉交給元件管家(ActivityManager執行緒)
  3. 元件管家執行任務(processNextBroadcast方法)的過程埋下定時炸彈
  4. 元件管家通知工地(receiver所在程式)的通訊員準備開始幹活
  5. 通訊員3號(binder_3)收到任務後轉交給包工頭(main主執行緒),加入包工頭的任務佇列(MessageQueue)
  6. 包工頭經過一番努力幹完活(完成receiver啟動的生命週期),發現當前程式還有SP正在執行寫入檔案的操作,便將向中控系統彙報的任務交給SP工人(queued-work-looper執行緒)
  7. SP工人歷經艱辛終於完成SP資料的持久化工作,便可以向中控系統彙報工作完成
  8. 中控系統的通訊員2號(binder_2)收到包工頭的完工彙報後,立刻拆除炸彈。如果在倒數計時結束前拆除炸彈則相安無事,否則會引發爆炸(觸發ANR)

(說明:SP從8.0開始採用名叫“queued-work-looper”的handler執行緒,在老版本採用newSingleThreadExecutor建立的單執行緒的執行緒池)

如果是動態廣播,或者靜態廣播沒有正在執行持久化操作的SP任務,則不需要經過“queued-work-looper”執行緒中轉,而是直接向中控系統彙報,流程更為簡單,如下圖所示:

broadcast_anr_2

可見,只有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並不會觸發provider超時。

provider_anr

圖解3:

  1. 客戶端(App程式)向中控系統(system_server程式)發起獲取內容提供者的請求
  2. 中控系統派出一名空閒的通訊員(binder_1)接收該請求,檢測到內容提供者尚未啟動,則先通過zygote孵化新程式
  3. 新孵化的provider程式向中控系統註冊自己的存在
  4. 中控系統的通訊員2號接收到該資訊後,向元件管家(ActivityManager執行緒)傳送訊息,埋下炸彈
  5. 通訊員2號通知工地(provider程式)的通訊員準備開始幹活
  6. 通訊員4號(binder_4)收到任務後轉交給包工頭(main主執行緒),加入包工頭的任務佇列(MessageQueue)
  7. 包工頭經過一番努力幹完活(完成provider的安裝工作)後向中控系統彙報工作已完成
  8. 中控系統的通訊員3號(binder_3)收到包工頭的完工彙報後,立刻拆除炸彈。如果在倒數計時結束前拆除炸彈則相安無事,否則會引發爆炸(觸發ANR)

更多細節詳見理解ContentProvider原理,gityuan.com/2016/07/30/…

inpu超時機制

input的超時檢測機制跟service、broadcast、provider截然不同,為了更好的理解input過程先來介紹兩個重要執行緒的相關工作:

  • InputReader執行緒負責通過EventHub(監聽目錄/dev/input)讀取輸入事件,一旦監聽到輸入事件則放入到InputDispatcher的mInBoundQueue佇列,並通知其處理該事件;
  • InputDispatcher執行緒負責將接收到的輸入事件分發給目標應用視窗,分發過程使用到3個事件佇列:
    • mInBoundQueue用於記錄InputReader傳送過來的輸入事件;
    • outBoundQueue用於記錄即將分發給目標應用視窗的輸入事件;
    • waitQueue用於記錄已分發給目標應用,且應用尚未處理完成的輸入事件;

input的超時機制並非時間到了一定就會爆炸,而是處理後續上報事件的過程才會去檢測是否該爆炸,所以更相信是掃雷的過程,具體如下圖所示。

input_anr

圖解4:

  1. InputReader執行緒通過EventHub監聽底層上報的輸入事件,一旦收到輸入事件則將其放至mInBoundQueue佇列,並喚醒InputDispatcher執行緒
  2. InputDispatcher開始分發輸入事件,設定埋雷的起點時間。先檢測是否有正在處理的事件(mPendingEvent),如果沒有則取出mInBoundQueue隊頭的事件,並將其賦值給mPendingEvent,且重置ANR的timeout;否則不會從mInBoundQueue中取出事件,也不會重置timeout。然後檢查視窗是否就緒(checkWindowReadyForMoreInputLocked),滿足以下任一情況,則會進入掃雷狀態(檢測前一個正在處理的事件是否超時),終止本輪事件分發,否則繼續執行步驟3。
    • 對於按鍵型別的輸入事件,則outboundQueue或者waitQueue不為空,
    • 對於非按鍵的輸入事件,則waitQueue不為空,且等待隊頭時間超時500ms
  3. 當應用視窗準備就緒,則將mPendingEvent轉移到outBoundQueue佇列
  4. 當outBoundQueue不為空,且應用管道對端連線狀態正常,則將資料從outboundQueue中取出事件,放入waitQueue佇列
  5. InputDispatcher通過socket告知目標應用所在程式可以準備開始幹活
  6. App在初始化時預設已建立跟中控系統雙向通訊的socketpair,此時App的包工頭(main執行緒)收到輸入事件後,會層層轉發到目標視窗來處理
  7. 包工頭完成工作後,會通過socket向中控系統彙報工作完成,則中控系統會將該事件從waitQueue佇列中移除。

input超時機制為什麼是掃雷,而非定時爆炸呢?是由於對於input來說即便某次事件執行時間超過timeout時長,只要使用者後續在沒有再生成輸入事件,則不會觸發ANR。 這裡的掃雷是指當前輸入系統中正在處理著某個耗時事件的前提下,後續的每一次input事件都會檢測前一個正在處理的事件是否超時(進入掃雷狀態),檢測當前的時間距離上次輸入事件分發時間點是否超過timeout時長。如果前一個輸入事件,則會重置ANR的timeout,從而不會爆炸。

更多細節詳見Input系統-ANR原理分析,gityuan.com/2017/01/01/…

ANR超時閾值

不同元件的超時閾值各有不同,關於service、broadcast、contentprovider以及input的超時閾值如下表:

anr_timeout

前臺與後臺服務的區別

系統對前臺服務啟動的超時為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時間片。

adj

關於細節詳見解讀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

除了前臺服務,前臺廣播,還有前臺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

ANR爆炸現場

對於service、broadcast、provider、input發生ANR後,中控系統會馬上去抓取現場的資訊,用於除錯分析。收集的資訊包括如下:

  • 將am_anr資訊輸出到EventLog,也就是說ANR觸發的時間點最接近的就是EventLog中輸出的am_anr資訊
  • 收集以下重要程式的各個執行緒呼叫棧trace資訊,儲存在data/anr/traces.txt檔案
    • 當前發生ANR的程式,system_server程式以及所有persistent程式
    • audioserver, cameraserver, mediaserver, surfaceflinger等重要的native程式
    • CPU使用率排名前5的程式
  • 將發生ANR的reason以及CPU使用情況資訊輸出到main log
  • 將traces檔案和CPU使用情況資訊儲存到dropbox,即data/system/dropbox目錄
  • 對使用者可感知的程式則彈出ANR對話方塊告知使用者,對使用者不可感知的程式發生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的理解。

相關文章