完全理解安卓應用無響應機制

引言

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

絕大多數人對ANR的瞭解僅停留在主線程耗時或CPU繁忙會致使ANR。面試過無數的候選人,幾乎沒有人能真正從系統級去梳理清晰ANR的前因後果,好比有哪些路徑會引起ANR? 有沒有可能主線程不耗時也出現ANR?如何更好的調試ANR?ios

若是沒有深刻研究過Android Framework的源代碼,是難以造成對ANR有一個全面、正確的理解。研究系統源碼以及工做實踐後提煉而來,以圖文並茂的方式跟你們講解,相信定能幫忙你們加深對ANR的理解。git

ANR觸發機制

對於知識學習的過程,要知其然知其因此然,才能作到庖丁解牛般遊刃有餘。要深刻理解ANR,就須要從根上去找尋答案,那就是ANR是如何觸發的?面試

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

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

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

service超時機制

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

service_anr

圖解1:app

  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,以下圖所示。socket

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的理解。

相關文章
相關標籤/搜索