天天看點

看完這篇 Android ANR 分析,就可以和面試官裝逼了!

ANR概述

首先,ANR(Application Not responding)是指應用程式未響應,Android系統對于一些事件需要在一定的時間範圍内完成,如果超過預定時間能未能得到有效響應或者響應時間過長,都會造成ANR。ANR由消息處理機制保證,Android在系統層實作了一套精密的機制來發現ANR,核心原理是消息排程和逾時處理。

其次,ANR機制主體實作在系統層。所有與ANR相關的消息,都會經過系統程序(system_server)排程,然後派發到應用程序完成對消息的實際處理,同時,系統程序設計了不同的逾時限制來跟蹤消息的處理。 一旦應用程式處理消息不當,逾時限制就起作用了,它收集一些系統狀态,譬如CPU/IO使用情況、程序函數調用棧,并且報告使用者有程序無響應了(ANR對話框)。

然後,ANR問題本質是一個性能問題。ANR機制實際上對應用程式主線程的限制,要求主線程在限定的時間内處理完一些最常見的操作(啟動服務、處理廣播、處理輸入), 如果處理逾時,則認為主線程已經失去了響應其他操作的能力。主線程中的耗時操作,譬如密集CPU運算、大量IO、複雜界面布局等,都會降低應用程式的響應能力。

哪些場景會造成ANR?

1. 發生ANR時會調用AppNotRespondingDialog.show()方法彈出對話框提示使用者,該對話框的依次調用關系如下圖所示:

看完這篇 Android ANR 分析,就可以和面試官裝逼了!

2. AppErrors.appNotResponding(),該方法是最終彈出ANR對話框的唯一入口,調用該方法的場景才會有ANR提示,也可以認為在主線程中執行無論再耗時的任務,隻要最終不調用該方法,都不會有ANR提示,也不會有ANR相關日志及報告;通過調用關系可以看出哪些場景會導緻ANR,有以下四種場景:

(1)Service Timeout:Service在特定的時間内無法處理完成

(2)BroadcastQueue Timeout:BroadcastReceiver在特定時間内無法處理完成

(3)ContentProvider Timeout:内容提供者執行逾時

(4)inputDispatching Timeout: 按鍵或觸摸事件在特定時間内無響應。

ANR機制

ANR機制可以分為兩部分:

ANR監測機制:Android對于不同的ANR類型(Broadcast, Service, InputEvent)都有一套監測機制。

ANR報告機制:在監測到ANR以後,需要顯示ANR對話框、輸出日志(發生ANR時的程序函數調用棧、CPU使用情況等)。

整個ANR機制的代碼也是橫跨了Android的幾個層:

App層:應用主線程的處理邏輯;

Framework層:ANR機制的核心,主要有AMS、BroadcastQueue、ActiveServices、InputmanagerService、InputMonitor、InputChannel、ProcessCpuTracker等;

Native層:InputDispatcher.cpp;

Provider逾時機制遇到的比較少,暫不做分析;Broadcast目前主要想說兩個知識點:

第一:無論是普通廣播還是有序廣播,最終廣播接受者的onreceive都是串行執行的,可以通過Demo進行驗證;

第二:通過Demo以及架構添加相關日志,都驗證了普通廣播也會有ANR監測機制,ANR機制以及問題分析文章認為隻有串行廣播才有ANR監測機制,後續再會專門講解Broadcast發送及接收流程,同時也會補充Broadcast ANR監測機制;本文主要以Servie處理逾時、輸入事件分發逾時為例探讨ANR監測機制。

Service逾時監測機制

Service運作在應用程式的主線程,如果Service的執行時間超過20秒,則會引發ANR。

當發生Service ANR時,一般可以先排查一下在Service的生命周期函數中(onCreate(), onStartCommand()等)有沒有做耗時的操作,譬如複雜的運算、IO操作等。 如果應用程式的代碼邏輯查不出問題,就需要深入檢查目前系統的狀态:CPU的使用情況、系統服務的狀态等,判斷當時發生ANR程序是否受到系統運作異常的影響。

如何檢測Service逾時呢?Android是通過設定定時消息實作的。定時消息是由AMS的消息隊列處理的(system_server的ActivityManager線程)。 AMS有Service運作的上下文資訊,是以在AMS中設定一套逾時檢測機制也是合情合理的。

我們先抛出兩個問題

問題一:Service啟動流程?

問題一:如何監測Service逾時?

主要通過以上兩個問題來說明Service監測機制,在知道Service啟動流程之後,通過Service啟動流程可以更容易分析Service逾時監測機制。

1. Service啟動流程如下圖所示:

看完這篇 Android ANR 分析,就可以和面試官裝逼了!

(1)ActiveServices.realStartServiceLocked()在通過app.thread的scheduleCreateService()來建立Service對象并調用Service.onCreate()後,接着又調用sendServiceArgsLocked()方法來調用Service的其他方法,如onStartCommand。以上兩步均是程序間通信,應用與AMS之間跨程序通信可以參考應用程序與系統程序通信

(2)以上隻是列出Service啟動流程的關鍵步驟,具體每個方法主要做哪些工作還需要檢視具體的代碼,暫時先忽略這些,感興趣的可以參考Android開發藝術探索等其他相關資料

2. Service逾時監測機制

Service逾時監測機制可以從Service啟動流程中找到。

(1)ActiveServices.realStartServiceLocked()主要工作有

    private final void realStartServiceLocked(ServiceRecord r,
            ProcessRecord app, boolean execInFg) throws RemoteException {
        ...
        // 主要是為了設定ANR逾時,可以看出在正式啟動Service之前開始ANR監測;
        bumpServiceExecutingLocked(r, execInFg, "create");
       // 啟動過程調用scheduleCreateService方法,最終會調用Service.onCreate方法;
        app.thread.scheduleCreateService(r, r.serviceInfo,
        // 綁定過程中,這個方法中會調用app.thread.scheduleBindService方法
        requestServiceBindingsLocked(r, execInFg);
        // 調動Service的其他方法,如onStartCommand,也是IPC通訊
        sendServiceArgsLocked(r, execInFg, true);
    }
           

(2)bumpServiceExecutingLocked()會調用scheduleServiceTimeoutLocked()方法

    void scheduleServiceTimeoutLocked(ProcessRecord proc) {
        if (proc.executingServices.size() == 0 || proc.thread == null) {
            return;
        }
        Message msg = mAm.mHandler.obtainMessage(
                ActivityManagerService.SERVICE_TIMEOUT_MSG);
        msg.obj = proc;
        // 在serviceDoneExecutingLocked中會remove該SERVICE_TIMEOUT_MSG消息,
        // 當逾時後仍沒有remove SERVICE_TIMEOUT_MSG消息,則執行ActiveServices. serviceTimeout()方法;
        mAm.mHandler.sendMessageDelayed(msg,
                proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
        // 前台程序中執行Service,SERVICE_TIMEOUT=20s;背景程序中執行Service,SERVICE_BACKGROUND_TIMEOUT=200s
    }
           

(3)如果在指定的時間内還沒有serviceDoneExecutingLocked()方法将消息remove掉,就會調用ActiveServices. serviceTimeout()方法

void serviceTimeout(ProcessRecord proc) {
    ...
    final long maxTime =  now -
              (proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
    ...
    // 尋找運作逾時的Service
    for (int i=proc.executingServices.size()-1; i>=0; i--) {
        ServiceRecord sr = proc.executingServices.valueAt(i);
        if (sr.executingStart < maxTime) {
            timeout = sr;
            break;
        }
       ...
    }
    ...
    // 判斷執行Service逾時的程序是否在最近運作程序清單,如果不在,則忽略這個ANR
    if (timeout != null && mAm.mLruProcesses.contains(proc)) {
        anrMessage = "executing service " + timeout.shortName;
    }
    ...
    if (anrMessage != null) {
        // 當存在timeout的service,則執行appNotResponding,報告ANR
        mAm.appNotResponding(proc, null, null, false, anrMessage);
    }
}
           

(4)Service onCreate逾時監測整體流程如下圖

看完這篇 Android ANR 分析,就可以和面試官裝逼了!

在onCreate生命周期開始執行前,啟動逾時監測,如果在指定的時間onCreate沒有執行完畢(該該方法中執行耗時任務),就會調用ActiveServices.serviceTimeout()方法報告ANR;如果在指定的時間内onCreate執行完畢,那麼就會調用ActivityManagerService.serviceDoneExecutingLocked()方法移除SERVICE_TIMEOUT_MSG消息,說明Service.onCreate方法沒有發生ANR,Service是由AMS排程,利用Handler和Looper,設計了一個TIMEOUT消息交由AMS線程來處理,整個逾時機制的實作都是在Java層;以上就是Service逾時監測的整體流程。

輸入事件逾時監測

應用程式可以接收輸入事件(按鍵、觸屏、軌迹球等),當5秒内沒有處理完畢時,則會引發ANR。

這裡先把問題抛出來了:

輸入事件經曆了一些什麼工序才能被派發到應用的界面?

如何檢測到輸入時間處理逾時?

1. Android輸入系統簡介

Android輸入系統總體流程與參與者如下圖所示。

看完這篇 Android ANR 分析,就可以和面試官裝逼了!

簡單來說,核心将原始事件寫入到裝置節點中,InputReader在其線程循環中不斷地從EventHub中抽取原始輸入事件,進行加工處理後将加工所得的事件放入InputDispatcher的派發發隊列中。InputDispatcher則在其線程循環中将派發隊列中的事件取出,查找合适的視窗,将事件寫入到視窗的事件接收管道中。視窗事件接收線程的Looper從管道中将事件取出,交由視窗事件處理函數進行事件響應。關鍵流程有:原始輸入事件的讀取與加工;輸入事件的派發;輸入事件的發送、接收與回報。其中輸入事件派發是指InputDispatcher不斷的從派發隊列取出事件、尋找合适的視窗進行發送的過程,輸入事件的發送是InputDispatcher通過Connection對象将事件發送給視窗的過程。

InputDispatcher與視窗之間的跨程序通信主要通過InputChannel來完成。在InputDispatcher與視窗通過InputChannel建立連接配接之後,就可以進行事件的發送、接收與回報;輸入事件的發送和接收主要流程如圖所示:

看完這篇 Android ANR 分析,就可以和面試官裝逼了!

其中,将輸入事件注入派發隊列後,會喚醒派發線程,派發線程循環由InputDispatcher.dispatchOnce函數完成;InputDispatcher将事件以InputMessage寫入InputChannel之後,視窗端的looper被喚醒,進而執行NativeInputReceiver::handleEvent()開始輸入事件的接收,從InputEventReceiver開始輸入事件被派發到使用者界面;以上隻是輸入事件的大緻流程,更詳細的流程可以參考相關資料;在了解輸入系統的大緻流程之後,我們來分析輸入事件的逾時監測機制。

2. 輸入事件逾時監測

按鍵事件逾時監測整體流程如下圖所示

看完這篇 Android ANR 分析,就可以和面試官裝逼了!

(1)InputDispatcher::dispatchOnceInnerLocked():

根據事件類型選擇不同僚件的處理方法:InputDispatcher::dispatchKeyLocked()或者InputDispatcher::dispatchMotionLocked(),我們以按鍵事件逾時監測為例進行說明;

(2)findFocusedWindowTargetsLocked()方法會調用checkWindowReadyForMoreInputLocked();該方法檢查視窗是否有能力再接收新的輸入事件;可能會有一系列的場景阻礙事件的繼續派發,相關場景有:

場景1: 視窗處于paused狀态,不能處理輸入事件

“Waiting because the [targetType] window is paused.”

場景2: 視窗還未向InputDispatcher注冊,無法将事件派發到視窗

“Waiting because the [targetType] window’s input channel is not registered with the input dispatcher. The window may be in the process of being removed.”

場景3: 視窗和InputDispatcher的連接配接已經中斷,即InputChannel不能正常工作

“Waiting because the [targetType] window’s input connection is [status]. The window may be in the process of being removed.”

場景4: InputChannel已經飽和,不能再處理新的事件

“Waiting because the [targetType] window’s input channel is full. Outbound queue length: %d. Wait queue length: %d.”

場景5: 對于按鍵類型(KeyEvent)的輸入事件,需要等待上一個事件處理完畢

“Waiting to send key event because the [targetType] window has not finished processing all of the input events that were previously delivered to it. Outbound queue length: %d. Wait queue length: %d.”

場景6: 對于觸摸類型(TouchEvent)的輸入事件,可以立即派發到目前的視窗,因為TouchEvent都是發生在使用者目前可見的視窗。但有一種情況, 如果目前應用由于隊列有太多的輸入事件等待派發,導緻發生了ANR,那TouchEvent事件就需要排隊等待派發。

“Waiting to send non-key event because the %s window has not finished processing certain input events that were delivered to it over %0.1fms ago. Wait queue length: %d. Wait queue head age: %0.1fms.”

以上這些場景就是我們常在日志中看到的ANR原因的列印。

(3)其中事件分發5s限制定義在InputDispatcher.cpp;InputDispatcher::handleTargetsNotReadyLocked()方法中如果事件5s之内還沒有分發完畢,則調用InputDispatcher::onANRLocked()提示使用者應用發生ANR;

//預設分發逾時間為5s
const nsecs_t DEFAULT_INPUT_DISPATCHING_TIMEOUT = 5000 * 1000000LL; 
int32_t InputDispatcher::handleTargetsNotReadyLocked(nsecs_t currentTime,
        const EventEntry* entry,
        const sp<InputApplicationHandle>& applicationHandle,
        const sp<InputWindowHandle>& windowHandle,
        nsecs_t* nextWakeupTime, const char* reason) {
    // 1.如果目前沒有聚焦視窗,也沒有聚焦的應用
    if (applicationHandle == NULL && windowHandle == NULL) {
        ...
    } else {
        // 2.有聚焦視窗或者有聚焦的應用
        if (mInputTargetWaitCause != INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY) {
            // 擷取等待的時間值
            if (windowHandle != NULL) {
                // 存在聚焦視窗,DEFAULT_INPUT_DISPATCHING_TIMEOUT事件為5s
                timeout = windowHandle->getDispatchingTimeout(DEFAULT_INPUT_DISPATCHING_TIMEOUT);
            } else if (applicationHandle != NULL) {
                // 存在聚焦應用,則擷取聚焦應用的分發逾時時間
                timeout = applicationHandle->getDispatchingTimeout(
                        DEFAULT_INPUT_DISPATCHING_TIMEOUT);
            } else {
                // 預設的分發逾時時間為5s
                timeout = DEFAULT_INPUT_DISPATCHING_TIMEOUT;
            }
        }
    }
    // 如果目前時間大于輸入目标等待逾時時間,即當逾時5s時進入ANR處理流程
    // currentTime 就是系統的目前時間,mInputTargetWaitTimeoutTime 是一個全局變量,
    if (currentTime >= mInputTargetWaitTimeoutTime) {
        // 調用ANR處理流程
        onANRLocked(currentTime, applicationHandle, windowHandle,
                entry->eventTime, mInputTargetWaitStartTime, reason);
        // 傳回需要等待處理
        return INPUT_EVENT_INJECTION_PENDING;
    } 
}
           

(4)當應用主線程被卡住的事件,再點選該應用其它元件也是無響應,因為事件派發是串行的,上一個事件不處理完畢,不會處理下一個事件。

(5)Activity.onCreate執行耗時操作,不管使用者如何操作都不會發生ANR,因為輸入事件相關監聽機制還沒有建立起來;InputChannel通道還沒有建立

這時是不會響應輸入事件,InputDispatcher還不能事件發送到應用視窗,ANR監聽機制也還沒有建立,是以此時是不會報告ANR的。

(6)輸入事件由InputDispatcher排程,待處理的輸入事件都會進入隊列中等待,設計了一個等待逾時的判斷,逾時機制的實作在Native層。

以上就是輸入事件ANR監測機制;具體邏輯請參考相關源碼;

ANR報告機制

無論哪種類型的ANR發生以後,最終都會調用 AppErrors.appNotResponding() 方法,所謂“殊途同歸”。這個方法的職能就是向使用者或開發者報告ANR發生了。 最終的表現形式是:彈出一個對話框,告訴使用者目前某個程式無響應;輸入一大堆與ANR相關的日志,便于開發者解決問題。

    final void appNotResponding(ProcessRecord app, ActivityRecord activity,
            ActivityRecord parent, boolean aboveSystem, final String annotation) {
        ...
        if (ActivityManagerService.MONITOR_CPU_USAGE) {
            // 1. 更新CPU使用資訊。ANR的第一次CPU資訊采樣,采樣資料會儲存在mProcessStats這個變量中
            mService.updateCpuStatsNow();
        }
            // 記錄ANR到EventLog中
            EventLog.writeEvent(EventLogTags.AM_ANR, app.userId, app.pid,
                    app.processName, app.info.flags, annotation);
        // 輸出ANR到main log.
        StringBuilder info = new StringBuilder();
        info.setLength(0);
        info.append("ANR in ").append(app.processName);
        if (activity != null && activity.shortComponentName != null) {
            info.append(" (").append(activity.shortComponentName).append(")");
        }
        info.append("\n");
        info.append("PID: ").append(app.pid).append("\n");
        if (annotation != null) {
            info.append("Reason: ").append(annotation).append("\n");
        }
        if (parent != null && parent != activity) {
            info.append("Parent: ").append(parent.shortComponentName).append("\n");
        }
        // 3. 列印調用棧。具體實作由dumpStackTraces()函數完成
        File tracesFile = ActivityManagerService.dumpStackTraces(
                true, firstPids,
                (isSilentANR) ? null : processCpuTracker,
                (isSilentANR) ? null : lastPids,
                nativePids);

        String cpuInfo = null;
        // MONITOR_CPU_USAGE預設為true
        if (ActivityManagerService.MONITOR_CPU_USAGE) {
            // 4. 更新CPU使用資訊。ANR的第二次CPU使用資訊采樣。兩次采樣的資料分别對應ANR發生前後的CPU使用情況
            mService.updateCpuStatsNow();
            synchronized (mService.mProcessCpuTracker) {
                // 輸出ANR發生前一段時間内各個程序的CPU使用情況
                cpuInfo = mService.mProcessCpuTracker.printCurrentState(anrTime);
            }
            // 輸出CPU負載
            info.append(processCpuTracker.printCurrentLoad());
            info.append(cpuInfo);
        }

        // 輸出ANR發生後一段時間内各個程序的CPU使用率
        info.append(processCpuTracker.printCurrentState(anrTime));
        //會列印發生ANR的原因,如輸入事件導緻ANR的不同場景
        Slog.e(TAG, info.toString());
        if (tracesFile == null) {
            // There is no trace file, so dump (only) the alleged culprit's threads to the log
            // 發送signal 3(SIGNAL_QUIT)來dump棧資訊
            Process.sendSignal(app.pid, Process.SIGNAL_QUIT);
        }

        // 将anr資訊同時輸出到DropBox
        mService.addErrorToDropBox("anr", app, app.processName, activity, parent, annotation,
                cpuInfo, tracesFile, null);
            // Bring up the infamous App Not Responding dialog
            // 5. 顯示ANR對話框。抛出SHOW_NOT_RESPONDING_MSG消息,
            // AMS.MainHandler會處理這條消息,顯示AppNotRespondingDialog對話框提示使用者發生ANR
            Message msg = Message.obtain();
            HashMap<String, Object> map = new HashMap<String, Object>();
            msg.what = ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG;
            msg.obj = map;
            msg.arg1 = aboveSystem ? 1 : 0;
            map.put("app", app);
            if (activity != null) {
                map.put("activity", activity);
            }

            mService.mUiHandler.sendMessage(msg);
        }
    }
           

除了主體邏輯,發生ANR時還會輸出各種類别的日志:

event log:通過檢索”am_anr”關鍵字,可以找到發生ANR的應用

main log:通過檢索”ANR in “關鍵字,可以找到ANR的資訊,日志的上下文會包含CPU的使用情況

dropbox:通過檢索”anr”類型,可以找到ANR的資訊

traces:發生ANR時,各程序的函數調用棧資訊

至此ANR相關報告已經完成,後續需要分析ANR問題,分析ANR往往是從main log中的CPU使用情況和traces中的函數調用棧開始。是以,更新CPU的使用資訊updateCpuStatsNow()方法和列印函數棧dumpStackTraces()方法,是系統報告ANR問題關鍵所在,具體分析ANR問題請參考相關資料。

總結

1. ANR的監測機制:首先分析Service和輸入事件大緻工作流程,然後從Service,InputEvent兩種不同的ANR監測機制的源碼實作開始,分析了Android如何發現各類ANR。在啟動服務、輸入事件分發時,植入逾時檢測,用于發現ANR。

2. ANR的報告機制:分析Android如何輸出ANR日志。當ANR被發現後,兩個很重要的日志輸出是:CPU使用情況和程序的函數調用棧,這兩類日志是我們解決ANR問題的利器。

3. 監測ANR的核心原理是消息排程和逾時處理。

4. 隻有被ANR監測的場景才會有ANR報告以及ANR提示框。

附錄

Android進階技術大綱,以及系統進階視訊;

Android進階技術大綱

Android進階進階視訊資料

擷取方式;

加Android進階群;701740775。即可前往免費領取。免費備注一下csdn

繼續閱讀