ANR機制及問題分析

原文鏈接:https://duanqz.github.io/2015-10-12-ANR-Analysis#213-input%E5%A4%84%E7%90%86%E8%B6%85%E6%97%B6

目錄

 

1. 概覽

2. ANR機制

2.1 ANR的監測機制

2.1.1 Service處理超時

2.1.2 Broadcast處理超時

2.1.3 Input處理超時

2.1.4 小結

2.2 ANR的報告機制

2.2.1 CPU的使用情況

2.2.2 函數調用棧

3. 問題分析方法

3.1 日誌獲取

3.2 問題定位

3.3 場景還原

3.3.1 第一個假設和驗證

3.3.2 第二個假設和驗證

4. 總結


1. 概覽

ANR(Application Not Responding),應用程序無響應,簡單一個定義,卻涵蓋了很多Android系統的設計思想。

首先,ANR屬於應用程序的範疇,這不同於SNR(System Not Respoding),SNR反映的問題是系統進程(system_server)失去了響應能力,而ANR明確將問題圈定在應用程序。 SNR由Watchdog機制保證,具體可以查閱Watchdog機制以及問題分析; ANR由消息處理機制保證,Android在系統層實現了一套精密的機制來發現ANR,核心原理是消息調度和超時處理。

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

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

最後,部分ANR問題是很難分析的,有時候由於系統底層的一些影響,導致消息調度失敗,出現問題的場景又難以復現。 這類ANR問題往往需要花費大量的時間去了解系統的一些行爲,超出了ANR機制本身的範疇。

2. ANR機制

分析一些初級的ANR問題,只需要簡單理解最終輸出的日誌即可,但對於一些由系統問題(譬如CPU負載過高、進程卡死)引發的ANR,就需要對整個ANR機制有所瞭解,才能定位出問題的原因。

ANR機制可以分爲兩部分:

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

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

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

下面我們會深入源碼,分析ANR的監測和報告過程。

2.1 ANR的監測機制

2.1.1 Service處理超時

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

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

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

Service ANR機制相對最爲簡單,主體實現在ActiveServices中。 當Service的生命週期開始時,bumpServiceExecutingLocked()會被調用,緊接着會調用scheduleServiceTimeoutLocked()

void scheduleServiceTimeoutLocked(ProcessRecord proc) {
    ...
    Message msg = mAm.mHandler.obtainMessage(
            ActivityManagerService.SERVICE_TIMEOUT_MSG);
    msg.obj = proc;
    // 通過AMS.MainHandler拋出一個定時消息
    mAm.mHandler.sendMessageAtTime(msg,
         proc.execServicesFg ? (now+SERVICE_TIMEOUT) : (now+ SERVICE_BACKGROUND_TIMEOUT));
}

上述方法通過AMS.MainHandler拋出一個定時消息SERVICE_TIMEOUT_MSG

  • 前臺進程中執行Service,超時時間是SERVICE_TIMEOUT(20秒)
  • 後臺進程中執行Service,超時時間是SERVICE_BACKGROUND_TIMEOUT(200秒)

當Service的生命週期結束時,會調用serviceDoneExecutingLocked()方法,之前拋出的SERVICE_TIMEOUT_MSG消息在這個方法中會被清除。 如果在超時時間內,SERVICE_TIMEOUT_MSG沒有被清除,那麼,AMS.MainHandler就會響應這個消息:

case SERVICE_TIMEOUT_MSG: {
    // 判斷是否在做dexopt操作, 該操作的比較耗時,允許再延長20秒
    if (mDidDexOpt) {
        mDidDexOpt = false;
        Message nmsg = mHandler.obtainMessage(SERVICE_TIMEOUT_MSG);
        nmsg.obj = msg.obj;
        mHandler.sendMessageDelayed(nmsg, ActiveServices.SERVICE_TIMEOUT);
        return;
    }
    mServices.serviceTimeout((ProcessRecord)msg.obj);
} break;

如果不是在做dexopt操作,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) {
        mAm.appNotResponding(proc, null, null, false, anrMessage);
    }
}

上述方法會找到當前進程已經超時的Service,經過一些判定後,決定要報告ANR,最終調用AMS.appNotResponding()方法。 走到這一步,ANR機制已經完成了監測報告任務,剩下的任務就是ANR結果的輸出,我們稱之爲ANR的報告機制。 ANR的報告機制是通過AMS.appNotResponding()完成的,Broadcast和InputEvent類型的ANR最終也都會調用這個方法,我們後文再詳細展開。

至此,我們分析了Service的ANR機制:

通過定時消息跟蹤Service的運行,當定時消息被響應時,說明Service還沒有運行完成,這就意味着Service ANR。

2.1.2 Broadcast處理超時

應用程序可以註冊廣播接收器,實現BroadcastReceiver.onReceive()方法來完成對廣播的處理。 通常,這個方法是在主線程執行的,Android限定它執行時間不能超過10秒,否則,就會引發ANR。

onReceive()也可以調度在其他線程執行,通過Context.registerReceiver(BroadcastReceiver, IntentFilter, String, Handler)這個方法註冊廣播接收器, 可以指定一個處理的Handler,將onReceive()調度在非主線程執行。

這裏先把問題拋出來了:

  1. Android如何將廣播投遞給各個應用程序?
  2. Android如何檢測廣播處理超時?

廣播消息的調度

AMS維護了兩個廣播隊列BroadcastQueue:

  • foreground queue,前臺隊列的超時時間是10秒
  • background queue,後臺隊列的超時時間是60秒

之所以有兩個,就是因爲要區分的不同超時時間。所有發送的廣播都會進入到隊列中等待調度,在發送廣播時,可以通過Intent.FLAG_RECEIVER_FOREGROUND參數將廣播投遞到前臺隊列。 AMS線程會不斷地從隊列中取出廣播消息派發到各個接收器(BroadcastReceiver)。當要派發廣播時,AMS會調用BroadcastQueue.scheduleBroadcastsLocked()方法:

public void scheduleBroadcastsLocked() {
    ...
    if (mBroadcastsScheduled) {
        return;
    }
    mHandler.sendMessage(mHandler.obtainMessage(BROADCAST_INTENT_MSG, this));
    mBroadcastsScheduled = true;
}

上述方法中,往AMS線程的消息隊列發送BROADCAST_INTENT_MSG消息,由此也可以看到真正派發廣播的是AMS線程(system_server進程中的ActivityManager線程)。 由於上述方法可能被併發調用,所以通過mBroadcastsScheduled這個變量來標識BROADCAST_INTENT_MSG是不是已經被AMS線程接收了,當已經拋出的消息還未被接受時,不需要重新拋出。 該消息被接收後的處理邏輯如下:

public void handleMessage(Message msg) {
    switch (msg.what) {
        case BROADCAST_INTENT_MSG: {
            ...
            processNextBroadcast(true);
        } break;
        ...
    }
}

直接調用BroadcastQueue.processNextBroadcast()方法,fromMsg參數爲true表示這是一次來自BROADCAST_INTENT_MSG消息的派發請求。 BroadcastQueue.processNextBroadcast()是派發廣播消息最爲核心的函數,代碼量自然也不小,我們分成幾個部分來分析:

// processNextBroadcast部分1:處理非串行廣播消息
final void  processNextBroadcast(boolean fromMsg) {
    ...
    // 1. 設置mBroadcastsScheduled
    if (fromMsg) {
        mBroadcastsScheduled = false;
    }
    // 2. 處理“並行廣播消息”
    while (mParallelBroadcasts.size() > 0) {
        ...
        final int N = r.receivers.size();
        for (int i=0; i<N; i++) {
            Object target = r.receivers.get(i);
            deliverToRegisteredReceiverLocked(r, (BroadcastFilter)target, false);
        }
        addBroadcastToHistoryLocked(r);
    }
    // 3. 處理阻塞的廣播消息
    if (mPendingBroadcast != null) {
        ...
        if (!isDead) {
            // isDead表示當前廣播消息的進程的存活狀態
            // 如果還活着,則返回該函數,繼續等待下次派發
            return;
        }
        ...
    }
//未完待續

第一個部分是處理非”串行廣播消息“,有以下幾個步驟:

  1. 設置mBroadcastsScheduled。該變量在前文說過,是對BROADCAST_INTENT_MSG進行控制。 如果是響應BROADCAST_INTENT_MSG的派發調用,則將mBroadcastsScheduled設爲false, 表示本次BROADCAST_INTENT_MSG已經處理完畢,可以繼續拋出下一次BROADCAST_INTENT_MSG消息了

  2. 處理“並行廣播消息”。廣播接受器有“動態”和“靜態”之分,通過Context.registerReceiver()註冊的廣播接收器爲“動態”的,通過AndroidManifest.xml註冊的廣播接收器爲“靜態”的。 廣播消息有“並行”和“串行”之分,“並行廣播消息”都會派發到“動態”接收器,“串行廣播消息”則會根據實際情況派發到兩種接收器。 我們先不去探究Android爲什麼這麼設計,只關注這兩種廣播消息派發的區別。在BroadcastQueue維護着兩個隊列:

    • mParallelBroadcasts,“並行廣播消息”都會進入到此隊列中排隊。“並行廣播消息”可以一次性派發完畢,即在一個循環中將廣播派發到所有的“動態”接收器

    • mOrderedBroadcasts,“串行廣播消息”都會進入到此隊列中排隊。“串行廣播消息”需要輪侯派發,當一個接收器處理完畢後,會再拋出BROADCAST_INTENT_MSG消息, 再次進入BroadcastQueue.processNextBroadcast()處理下一個

  3. 處理阻塞的廣播消息。有時候會存在一個廣播消息派發不出去的情況,這個廣播消息會保存在mPendingBroadcast變量中。新一輪的派發啓動時,會判斷接收該消息的進程是否還活着, 如果接收進程還活着,那麼就繼續等待。否則,就放棄這個廣播消息

接下來是最爲複雜的一部分,處理“串行廣播消息”,ANR監測機制只在這一類廣播消息中才發揮作用,也就是說“並行廣播消息”是不會發生ANR的。

// processNextBroadcast部分2:從隊列中取出“串行廣播消息”
    do {
        r = mOrderedBroadcasts.get(0);
        // 1. 廣播消息的第一個ANR監測機制
        if (mService.mProcessesReady && r.dispatchTime > 0) {
            if ((numReceivers > 0) &&
                (now > r.dispatchTime + (2*mTimeoutPeriod*numReceivers))) {
                broadcastTimeoutLocked(false); // forcibly finish this broadcast
                ...
        }
        // 2. 判斷該廣播消息是否處理完畢
        if (r.receivers == null || r.nextReceiver >= numReceivers ||
            r.resultAbort || forceReceive) {
            ...
            cancelBroadcastTimeoutLocked();
            ...
            mOrderedBroadcasts.remove(0);
            continue;
        }

    } while (r == null);
//未完待續

這部分是一個do-while循環,每次都從mOrderedBroadcasts隊列中取出第一條廣播消息進行處理。第一個Broadcast ANR監測機制千呼萬喚總算是出現了:

  1. 判定當前時間是否已經超過了r.dispatchTime + 2×mTimeoutPeriod×numReceivers:

    • dispatchTime表示這一系列廣播消息開始派發的時間。“串行廣播消息”是逐個接收器派發的,一個接收器處理完畢後,纔開始處理下一個消息派發。 開始派發到第一個接收器的時間就是dispatchTime。dispatchTime需要開始等廣播消息派發以後纔會設定,也就是說,第一次進入processNextBroadcast()時, dispatchTime=0,並不會進入該條件判斷

    • mTimeoutPeriod由當前BroadcastQueue的類型決定(forground爲10秒,background爲60秒)。這個時間在初始化BroadcastQueue的時候就設置好了, 本意是限定每一個Receiver處理廣播的時間,這裏利用它做了一個超時計算

    假設一個廣播消息有2個接受器,mTimeoutPeriod是10秒,當2×10×2=40秒後,該廣播消息還未處理完畢,就調用broadcastTimeoutLocked()方法, 這個方法會判斷當前是不是發生了ANR,我們後文再分析。

  2. 如果廣播消息是否已經處理完畢,則從mOrderedBroadcasts中移除,重新循環,處理下一條;否則,就會跳出循環。

以上代碼塊完成的主要任務是從隊列中取一條“串行廣播消息”,接下來就準備派發了:

// processNextBroadcast部分3:串行廣播消息的第二個ANR監測機制
    r.receiverTime = SystemClock.uptimeMillis();
    ...
    if (! mPendingBroadcastTimeoutMessage) {
        long timeoutTime = r.receiverTime + mTimeoutPeriod;
        ...
        setBroadcastTimeoutLocked(timeoutTime);
    }
//未完待續

取出“串行廣播消息”後,一旦要開始派發,第二個ANR檢測機制就出現了。mPendingBroadcastTimeoutMessage變量用於標識當前是否有阻塞的超時消息, 如果沒有則調用BroadcastQueue.setBroadcastTimeoutLocked()

final void setBroadcastTimeoutLocked(long timeoutTime) {
    if (! mPendingBroadcastTimeoutMessage) {
        Message msg = mHandler.obtainMessage(BROADCAST_TIMEOUT_MSG, this);
        mHandler.sendMessageAtTime(msg, timeoutTime);
        mPendingBroadcastTimeoutMessage = true;
    }
}

通過設置一個定時消息BROADCAST_TIMEOUT_MSG來跟蹤當前廣播消息的執行情況,這種超時監測機制跟Service ANR很類似,也是拋到AMS線程的消息隊列。 如果所有的接收器都處理完畢了,則會調用cancelBroadcastTimeoutLocked()清除該消息;否則,該消息就會響應,並調用broadcastTimeoutLocked(), 這個方法在第一種ANR監測機制的時候調用過,第二種ANR監測機制也會調用,我們留到後文分析。

設置完定時消息後,就開始派發廣播消息了,首先是“動態”接收器:

// processNextBroadcast部分4: 向“動態”接收器派發廣播消息
    final Object nextReceiver = r.receivers.get(recIdx);
    // 動態接收器的類型都是BroadcastFilter
    if (nextReceiver instanceof BroadcastFilter) {
        BroadcastFilter filter = (BroadcastFilter)nextReceiver;
        deliverToRegisteredReceiverLocked(r, filter, r.ordered);
        ...
        return;
    }
//未完待續

“動態”接收器的載體進程一般是處於運行狀態的,所以向這種類型的接收器派發消息相對簡單,調用BroadcastQueue.deliverToRegisteredReceiverLocked()完成接下來的工作。 但“靜態”接收器是在AndroidManifest.xml中註冊的,派發的時候,可能廣播接收器的載體進程還沒有啓動,所以,這種場景會複雜很多。

// processNextBroadcast部分5: 向“靜態”接收器派發廣播消息
    // 靜態接收器的類型都是 ResolveInfo
    ResolveInfo info = (ResolveInfo)nextReceiver;
    ...
    // 1. 權限檢查
    ComponentName component = new ComponentName(
                info.activityInfo.applicationInfo.packageName,
                info.activityInfo.name);
    int perm = mService.checkComponentPermission(info.activityInfo.permission,
                r.callingPid, r.callingUid, info.activityInfo.applicationInfo.uid,
                info.activityInfo.exported);
    ...
    // 2. 獲取接收器所在的進程
    ProcessRecord app = mService.getProcessRecordLocked(targetProcess,
                info.activityInfo.applicationInfo.uid, false);
    // 3. 進程已經啓動
    if (app != null && app.thread != null) {
       ...
       processCurBroadcastLocked(r, app);
       return;
    }
    // 4. 進程還未啓動
    if ((r.curApp=mService.startProcessLocked(targetProcess,
                info.activityInfo.applicationInfo, true,
                r.intent.getFlags() | Intent.FLAG_FROM_BACKGROUND,
                "broadcast", r.curComponent,
                (r.intent.getFlags()&Intent.FLAG_RECEIVER_BOOT_UPGRADE) != 0, false, false))
                        == null) {
        ...
        scheduleBroadcastsLocked();
        return;
    }
    // 5. 進程啓動失敗
    mPendingBroadcast = r;
    mPendingBroadcastRecvIndex = recIdx;
}
// processNextBroadcast完
  1. “靜態”接收器是ResolveInfo,需要通過PackageManager獲取包信息,進行權限檢查。權限檢查的內容非常龐大,此處不表。

  2. 經過一系列複雜的權限檢查後,終於可以向目標接收器派發了。通過AMS.getProcessRecordLocked()獲取廣播接收器的進程信息

  3. 如果app.thread != null,則進程已經啓動,就可以調用BroadcastQueue.processCurBroadcastLocked()進行接下來的派發處理了

  4. 如果進程還沒有啓動,則需要通過AMS.startProcessLocked()來啓動進程,當前消息並未派發,調用BroadcastQueue.scheduleBroadcastsLocked()進入下一次的調度

  5. 如果進程啓動失敗了,則當前消息記錄成mPendingBroadcast,即阻塞的廣播消息,等待下一次調度時處理

龐大的processNextBroadcast()終於完結了,它的功能就是對廣播消息進行調度,該方法被設計得十分複雜而精巧,用於應對不同的廣播消息和接收器的處理。

廣播消息的跨進程傳遞

調度是完成了,接下來,我們就來分析被調度廣播消息如何到達應用程序。上文的分析中,最終有兩個方法將廣播消息派發出去: BroadcastQueue.deliverToRegisteredReceiverLocked()BroadcastQueue.processCurBroadcastLocked()

我們先不展開這兩個函數的邏輯,試想要將廣播消息的從AMS線程所在的system_server進程傳遞到應用程序的進程,該怎麼實現? 自然需要用到跨進程調用,Android中最常規的手段就是Binder機制。沒錯,廣播消息派發到應用進程就是這麼玩的。

對於應用程序已經啓動(app.thread != null)的情況,會通過IApplicationThread發起跨進程調用, 調用關係如下:

ActivityThread.ApplicationThread.scheduleReceiver()
└── ActivityThread.handleReceiver()
    └── BroadcastReceiver.onReceive()

對於應用程序還未啓動的情況,會調用IIntentReceiver發起跨進程調用,應用進程的實現在LoadedApk.ReceiverDispatcher.IntentReceiver中, 調用關係如下:

LoadedApk.ReceiverDispatcher.IntentReceiver.performReceive()
└── LoadedApk.ReceiverDispatcher.performReceiver()
    └── LoadedApk.ReceiverDispatcher.Args.run()
        └── BroadcastReceiver.onReceive()

最終,都會調用到BroadcastReceiver.onReceive(),在應用進程執行接收廣播消息的具體動作。 對於“串行廣播消息”而言,執行完了以後,還需要通知system_server進程,才能繼續將廣播消息派發到下一個接收器,這又需要跨進程調用了。 應用進程在處理完廣播消息後,即在BroadcastReceiver.onReceive()執行完畢後,會調用BroadcastReceiver.PendingResult.finish(), 接下來的調用關係如下:

BroadcastReceiver.PendingResult.finish()
└── BroadcastReceiver.PendingResult.sendFinished()
    └── IActivityManager.finishReceiver()
        └── ActivityManagerService.finishReceiver()
            └── BroadcastQueue.processNextBroadcat()

通過IActivityManager發起了一個從應用進程到system_server進程的調用,最終在AMS線程中,又走到了BroadcastQueue.processNextBroadcat(), 開始下一輪的調度。

broadcastTimeoutLocked()方法

前文說過,兩種ANR機制最終都會調用BroadcastQueue.broadcastTimeoutLocked()方法, 第一種ANR監測生效時,會將fromMsg設置爲false;第二種ANR監測生效時,會將fromMsg參數爲True時,表示當前正在響應BROADCAST_TIMEOUT_MSG消息。

final void broadcastTimeoutLocked(boolean fromMsg) {
    // 1. 設置mPendingBroadcastTimeoutMessage
    if (fromMsg) {
        mPendingBroadcastTimeoutMessage = false;
    }
    ...
    // 2. 判斷第二種ANR機制是否超時
    BroadcastRecord r = mOrderedBroadcasts.get(0);
    if (fromMsg) {
        long timeoutTime = r.receiverTime + mTimeoutPeriod;
        if (timeoutTime > now) {
            setBroadcastTimeoutLocked(timeoutTime);
            return;
        }
    }
    ...
    // 3. 已經超時,則結束對當前接收器,開始新一輪調度
    finishReceiverLocked(r, r.resultCode, r.resultData,
                r.resultExtras, r.resultAbort, false);
    scheduleBroadcastsLocked();

    // 4. 拋出繪製ANR對話框的消息
    if (anrMessage != null) {
        mHandler.post(new AppNotResponding(app, anrMessage));
    }
}
  1. mPendingBroadcastTimeoutMessage標識是否存在未處理的BROADCAST_TIMEOUT_MSG消息, 將其設置成false,允許繼續拋出BROADCAST_TIMEOUT_MSG消息

  2. 每次將廣播派發到接收器,都會將r.receiverTime更新,如果判斷當前還未超時,則又拋出一個BROADCAST_TIMEOUT_MSG消息。 正常情況下,所有接收器處理完畢後,纔會清除BROADCAST_TIMEOUT_MSG;否則,每進行一次廣播消息的調度,都會拋出BROADCAST_TIMEOUT_MSG消息

  3. 判斷已經超時了,說明當前的廣播接收器還未處理完畢,則結束掉當前的接收器,開始新一輪廣播調度

  4. 最終,發出繪製ANR對話框的消息

至此,我們回答了前文提出的兩個問題:

AMS維護着廣播隊列BroadcastQueue,AMS線程不斷從隊列中取出消息進行調度,完成廣播消息的派發。 在派發“串行廣播消息”時,會拋出一個定時消息BROADCAST_TIMEOUT_MSG,在廣播接收器處理完畢後,AMS會將定時消息清除。 如果BROADCAST_TIMEOUT_MSG得到了響應,就會判斷是否廣播消息處理超時,最終通知ANR的發生。

2.1.3 Input處理超時

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

如果Broadcast ANR一樣,我們拋出Input ANR的幾個問題:

  1. 輸入事件經歷了一些什麼工序才能被派發到應用的界面?
  2. 如何檢測到輸入時間處理超時?

輸入事件最開始由硬件設備(譬如按鍵或觸摸屏幕)發起,Android有一套輸入子系統來發現各種輸入事件, 這些事件最終都會被InputDispatcher分發到各個需要接收事件的窗口。 那麼,窗口如何告之InputDispatcher自己需要處理輸入事件呢?Android通過InputChannel 連接InputDispatcher和窗口,InputChannel其實是封裝後的Linux管道(Pipe)。 每一個窗口都會有一個獨立的InputChannel,窗口需要將這個InputChannel註冊到InputDispatcher中:

status_t InputDispatcher::registerInputChannel(const sp<InputChannel>& inputChannel,
        const sp<InputWindowHandle>& inputWindowHandle, bool monitor) {
    ...
    sp<Connection> connection = new Connection(inputChannel, inputWindowHandle, monitor);
    int fd = inputChannel->getFd();
    mConnectionsByFd.add(fd, connection);
    ...
    mLooper->addFd(fd, 0, ALOOPER_EVENT_INPUT, handleReceiveCallback, this);
    ...
    mLooper->wake();
    return OK;
}

對於InputDispatcher而言,每註冊一個InputChannel都被視爲一個Connection,通過文件描述符來區別。InputDispatcher是一個消息處理循環,當有新的Connection時,就需要喚醒消息循環隊列進行處理。

輸入事件的類型有很多,按鍵、軌跡球、觸屏等,Android對這些事件進行了分類,處理這些事件的窗口也被賦予了一個類型(targetType):Foucused或Touched, 如果當前輸入事件是按鍵類型,則尋找Focused類型的窗口;如果當前輸入事件類型是觸摸類型,則尋找Touched類型的窗口。 InputDispatcher需要經過以下複雜的調用關係,才能把一個輸入事件派發出去(調用關係以按鍵事件爲例,觸屏事件的調用關係類似):

InputDispatcherThread::threadLoop()
└── InputDispatcher::dispatchOnce()
    └── InputDispatcher::dispatchOnceInnerLocked()
        └── InputDispatcher::dispatchKeyLocked()
            └── InputDispatcher::dispatchEventLocked()
                └── InputDispatcher::prepareDispatchCycleLocked()
                    └── InputDispatcher::enqueueDispatchEntriesLocked()
                        └── InputDispatcher::startDispatchCycleLocked()
                            └── InputPublisher::publishKeyEvent()

具體每個函數的實現邏輯此處不表。我們提煉出幾個關鍵點:

  • InputDispatcherThread是一個線程,它處理一次消息的派發
  • 輸入事件作爲一個消息,需要排隊等待派發,每一個Connection都維護兩個隊列:
    • outboundQueue: 等待發送給窗口的事件。每一個新消息到來,都會先進入到此隊列
    • waitQueue: 已經發送給窗口的事件
  • publishKeyEvent完成後,表示事件已經派發了,就將事件從outboundQueue挪到了waitQueue

事件經過這麼一輪處理,就算是從InputDispatcher派發出去了,但事件是不是被窗口收到了,還需要等待接收方的“finished”通知。 在向InputDispatcher註冊InputChannel的時候,同時會註冊一個回調函數handleReceiveCallback():

int InputDispatcher::handleReceiveCallback(int fd, int events, void* data) {
    ...
    for (;;) {
        ...
        status = connection->inputPublisher.receiveFinishedSignal(&seq, &handled);
        if (status) {
            break;
        }
        d->finishDispatchCycleLocked(currentTime, connection, seq, handled);
        ...
    }
    ...
    d->unregisterInputChannelLocked(connection->inputChannel, notify);
}

當收到的status爲OK時,會調用finishDispatchCycleLocked()來完成一個消息的處理:

InputDispatcher::finishDispatchCycleLocked()
└── InputDispatcher::onDispatchCycleFinishedLocked()
    └── InputDispatcher::doDispatchCycleFinishedLockedInterruptible()
        └── InputDispatcher::startDispatchCycleLocked()

調用到doDispatchCycleFinishedLockedInterruptible()方法時,會將已經成功派發的消息從waitQueue中移除, 進一步調用會startDispatchCycleLocked開始派發新的事件。

至此,我們回答了第一個問題:

一個正常的輸入事件會經過從outboundQueue挪到waitQueue的過程,表示消息已經派發出去;再經過從waitQueue中移除的過程,表示消息已經被窗口接收。 InputDispatcher作爲中樞,不停地在遞送着輸入事件,當一個事件無法得到處理的時候,InputDispatcher不能就此死掉啊,否則系統也太容易崩潰了。 InputDispatcher的策略是放棄掉處理不過來的事件,併發出通知(這個通知機制就是ANR),繼續進行下一輪消息的處理。

理解輸入事件分發模型,我們可以舉一個生活中的例子:
每一個輸入事件可以比做一個快遞,InputDispatcher就像一個快遞中轉站,窗口就像是收件人,InputChannel就像是快遞員。 所有快遞都會經過中轉站中處理,中轉站需要知道每一個快遞的收件人是誰,通過快遞員將快遞發送到具體的收件人。 這其中有很多場景導致快遞不能及時送到:譬如聯繫不到收件人;快遞很多,快遞員會忙不過來;快遞員受傷休假了等等… 這時候快遞員就需要告知中轉站:有快遞無法及時送到了。中轉站在收到快遞員的通知後,一邊繼續派發其他快遞,一邊報告上級。

在瞭解輸入事件分發模型之後,我們可以見識一下ANR機制了。在派發事件時,dispatchKeyLocked()dispatchMotionLocked(), 需要找到當前的焦點窗口,焦點窗口才是最終接收事件的地方,找窗口的過程就會判斷是否已經發生了ANR:

InputDispatcher::findFocusedWindowTargetsLocked()
InputDispatcher::findTouchedWindowTargetsLocked()
└── InputDispatcher::handleTargetsNotReadyLocked()
    └── InputDispatcher::onANRLocked()
        └── InputDispatcher::doNotifyANRLockedInterruptible()
            └── NativeInputManager::notifyANR()
  • 首先,會調用findFocusedWindowTargetsLocked()findTouchedWindowTargetsLocked()尋找接收輸入事件的窗口。

    在找到窗口以後,會調用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.”

  • 然後,上述有任何一個場景發生了,則輸入事件需要繼續等待,緊接着就會調用handleTargetsNotReadyLocked()來判斷是不是已經的等待超時了:

int32_t InputDispatcher::handleTargetsNotReadyLocked(nsecs_t currentTime,
        const EventEntry* entry,
        const sp<InputApplicationHandle>& applicationHandle,
        const sp<InputWindowHandle>& windowHandle,
        nsecs_t* nextWakeupTime, const char* reason) {
    ...
    if (currentTime >= mInputTargetWaitTimeoutTime) {
        onANRLocked(currentTime, applicationHandle, windowHandle,
            entry->eventTime, mInputTargetWaitStartTime, reason);
        *nextWakeupTime = LONG_LONG_MIN;
        return INPUT_EVENT_INJECTION_PENDING;
    }
    ...
}
  • 最後,如果當前事件派發已經超時,則說明已經檢測到了ANR,調用onANRLocked()方法,然後將nextWakeupTime設置爲最小值,馬上開始下一輪調度。 在onANRLocked()方法中, 會保存ANR的一些狀態信息,調用doNotifyANRLockedInterruptible(),進一步會調用到JNI層的 NativeInputManager::notifyANR()方法, 它的主要功能就是銜接Native層和Java層,直接調用Java層的InputManagerService.notifyANR()方法。
nsecs_t NativeInputManager::notifyANR(
    const sp<InputApplicationHandle>& inputApplicationHandle,
    const sp<InputWindowHandle>& inputWindowHandle,
    const String8& reason) {
    ...
    JNIEnv* env = jniEnv();

    // 將應用程序句柄、窗口句柄、ANR原因字符串,轉化爲Java層的對象
    jobject inputApplicationHandleObj =
            getInputApplicationHandleObjLocalRef(env, inputApplicationHandle);
    jobject inputWindowHandleObj =
            getInputWindowHandleObjLocalRef(env, inputWindowHandle);
    jstring reasonObj = env->NewStringUTF(reason.string());

    // 調用Java層的InputManagerService.notifyANR()方法
    jlong newTimeout = env->CallLongMethod(mServiceObj,
                gServiceClassInfo.notifyANR, inputApplicationHandleObj, inputWindowHandleObj,
                reasonObj);
    ...
    return newTimeout;
}

至此,ANR的處理邏輯轉交到了Java層。底層(Native)發現一旦有輸入事件派發超時,就會通知上層(Java),上層收到ANR通知後,決定是否終止當前輸入事件的派發。

發生ANR時,Java層最開始的入口是InputManagerService.notifyANR(),它是直接被Native層調用的。我們先把ANR的Java層調用關係列出來:

InputManagerService.notifyANR()
└── InputMonitor.notifyANR()
    ├── IApplicationToken.keyDispatchingTimedOut()
    │   └── ActivityRecord.keyDispatchingTimedOut()
    │       └── AMS.inputDispatchingTimedOut()
    │           └── AMS.appNotResponding()
    │
    └── AMS.inputDispatchingTimedOut()
        └── AMS.appNotResponding()
  • InputManagerService.notifyANR()只是爲Native層定義了一個接口,它直接調用InputMonitor.notifyANR()。 如果該方法的返回值等於0,則放棄本次輸入事件;如果大於0,則表示需要繼續等待的時間。
public long notifyANR(InputApplicationHandle inputApplicationHandle,
      InputWindowHandle inputWindowHandle, String reason) {
    ...
    if (appWindowToken != null && appWindowToken.appToken != null) {
        // appToken實際上就是當前的ActivityRecord。
        // 如果發生ANR的Activity還存在,則直接通過ActivityRecord通知事件派發超時
        boolean abort = appWindowToken.appToken.keyDispatchingTimedOut(reason);
        if (! abort) {
            return appWindowToken.inputDispatchingTimeoutNanos;
        }
    } else if (windowState != null) {
        // 如果發生ANR的Activity已經銷燬了,則通過AMS通知事件派發超時
        long timeout = ActivityManagerNative.getDefault().inputDispatchingTimedOut(
                        windowState.mSession.mPid, aboveSystem, reason);
         if (timeout >= 0) {
             return timeout;
         }
    }
    return 0; // abort dispatching
}
  • 上述方法中有兩種不同的調用方式,但最終都會交由AMS.inputDispatchingTimedOut()處理。AMS有重載的inputDispatchingTimedOut()方法,他們的參數不一樣。 ActivityRecord調用時,可以傳入的信息更多一點(當前發生ANR的界面是哪一個)。
@Override
public long inputDispatchingTimedOut(int pid, final boolean aboveSystem, String reason) {
    // 1. 根據進程號獲取到ProcessRecord
    proc = mPidsSelfLocked.get(pid);
    ...
    // 2. 獲取超時時間
    // 測試環境下的超時時間是INSTRUMENTATION_KEY_DISPATCHING_TIMEOUT(60秒),
    // 正常環境下的超時時間是KEY_DISPATCHING_TIMEOUT(5秒)
    timeout = getInputDispatchingTimeoutLocked(proc);
    // 調用重載的函數,如果返回True,則表示需要中斷當前的事件派發;
    if (!inputDispatchingTimedOut(proc, null, null, aboveSystem, reason)) {
        return -1;
    }
    // 3. 返回繼續等待的時間,這個值會傳遞到Native層
    return timeout;
}

public boolean inputDispatchingTimedOut(final ProcessRecord proc,
        final ActivityRecord activity, final ActivityRecord parent,
        final boolean aboveSystem, String reason) {
    ...
    // 1. 發生ANR進程正處於調試狀態,不需要中斷事件
    if (proc.debugging) {
        return false;
    }
    // 2. 當前正在做dexopt操作,這會比較耗時,不需要中斷
    if (mDidDexOpt) {
        // Give more time since we were dexopting.
        mDidDexOpt = false;
        return false;
    }
    // 3. 發生ANR的進程是測試進程,需要中斷,但不在UI界面顯示ANR信息判斷
    if (proc.instrumentationClass != null) {
        ...
        finishInstrumentationLocked(proc, Activity.RESULT_CANCELED, info);
        return true;
    }

    // 4. 通知UI界面顯示ANR信息
    mHandler.post(new Runnable() {
        @Override
        public void run() {
            appNotResponding(proc, activity, parent, aboveSystem, annotation);
        }
    });
    ...
    return true;
}

至此,我們回答了第二個問題:

在InputDispatcher派發輸入事件時,會尋找接收事件的窗口,如果無法正常派發,則可能會導致當前需要派發的事件超時(默認是5秒)。 Native層發現超時了,會通知Java層,Java層經過一些處理後,會反饋給Native層,是繼續等待還是丟棄當前派發的事件。

2.1.4 小結

ANR監測機制包含三種:

  • Service ANR,前臺進程中Service生命週期不能超過20秒,後臺進程中Service的生命週期不能超過200秒。 在啓動Service時,拋出定時消息SERVICE_TIMEOUT_MSGSERVICE_BACKGOURND_TIMEOUT_MSG,如果定時消息響應了,則說明發生了ANR

  • Broadcast ANR,前臺的“串行廣播消息”必須在10秒內處理完畢,後臺的“串行廣播消息”必須在60秒處理完畢, 每派發串行廣播消息到一個接收器時,都會拋出一個定時消息BROADCAST_TIMEOUT_MSG,如果定時消息響應,則判斷是否廣播消息處理超時,超時就說明發生了ANR

  • Input ANR,輸入事件必須在5秒內處理完畢。在派發一個輸入事件時,會判斷當前輸入事件是否需要等待,如果需要等待,則判斷是否等待已經超時,超時就說明發生了ANR

ANR監測機制實際上是對應用程序主線程的要求,要求主線成必須在限定的時間內,完成對幾種操作的響應;否則,就可以認爲應用程序主線程失去響應能力。

從ANR的三種監測機制中,我們看到不同超時機制的設計:

Service和Broadcast都是由AMS調度,利用Handler和Looper,設計了一個TIMEOUT消息交由AMS線程來處理,整個超時機制的實現都是在Java層; InputEvent由InputDispatcher調度,待處理的輸入事件都會進入隊列中等待,設計了一個等待超時的判斷,超時機制的實現在Native層。

2.2 ANR的報告機制

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

最終形式我們見過很多,但輸出日誌的原理是什麼,未必所有人都瞭解,下面我們就來認識一下是如何輸出ANR日誌的。

final void appNotResponding(ProcessRecord app, ActivityRecord activity,
        ActivityRecord parent, boolean aboveSystem, final String annotation) {
    // app: 當前發生ANR的進程
    // activity: 發生ANR的界面
    // parent: 發生ANR的界面的上一級界面
    // aboveSystem:
    // annotation: 發生ANR的原因
    ...
    // 1. 更新CPU使用信息。ANR的第一次CPU信息採樣
    updateCpuStatsNow();
    ...
    // 2. 填充firstPids和lastPids數組。從最近運行進程(Last Recently Used)中挑選:
    //    firstPids用於保存ANR進程及其父進程,system_server進程和persistent的進程(譬如Phone進程)
    //    lastPids用於保存除firstPids外的其他進程
    firstPids.add(app.pid);
    int parentPid = app.pid;
    if (parent != null && parent.app != null && parent.app.pid > 0)
        parentPid = parent.app.pid;
    if (parentPid != app.pid) firstPids.add(parentPid);
    if (MY_PID != app.pid && MY_PID != parentPid) firstPids.add(MY_PID);

    for (int i = mLruProcesses.size() - 1; i >= 0; i--) {
        ProcessRecord r = mLruProcesses.get(i);
        if (r != null && r.thread != null) {
            int pid = r.pid;
            if (pid > 0 && pid != app.pid && pid != parentPid && pid != MY_PID) {
                if (r.persistent) {
                    firstPids.add(pid);
                } else {
                    lastPids.put(pid, Boolean.TRUE);
                }
            }
        }
    }
    ...
    // 3. 打印調用棧
    File tracesFile = dumpStackTraces(true, firstPids, processCpuTracker, lastPids,
                NATIVE_STACKS_OF_INTEREST);
    ...
    // 4. 更新CPU使用信息。ANR的第二次CPU使用信息採樣
    updateCpuStatsNow();
    ...
    // 5. 顯示ANR對話框
    Message msg = Message.obtain();
    HashMap<String, Object> map = new HashMap<String, Object>();
    msg.what = SHOW_NOT_RESPONDING_MSG;
    ...
    mHandler.sendMessage(msg);
}

該方法的主體邏輯可以分成五個部分來看:

  1. 更新CPU的統計信息。這是發生ANR時,第一次CPU使用信息的採樣,採樣數據會保存在mProcessStats這個變量中

  2. 填充firstPids和lastPids數組。當前發生ANR的應用會首先被添加到firstPids中,這樣打印函數棧的時候,當前進程總是在trace文件的最前面

  3. 打印函數調用棧(StackTrace)。具體實現由dumpStackTraces()函數完成

  4. 更新CPU的統計信息。這是發生ANR時,第二次CPU使用信息的採樣,兩次採樣的數據分別對應ANR發生前後的CPU使用情況

  5. 顯示ANR對話框。拋出SHOW_NOT_RESPONDING_MSG消息,AMS.MainHandler會處理這條消息,顯示AppNotRespondingDialog

當然,除了主體邏輯,發生ANR時還會輸出各種類別的日誌:

  • event log,通過檢索”am_anr”關鍵字,可以找到發生ANR的應用
  • main log,通過檢索”ANR in “關鍵字,可以找到ANR的信息,日誌的上下文會包含CPU的使用情況
  • dropbox,通過檢索”anr”類型,可以找到ANR的信息
  • traces, 發生ANR時,各進程的函數調用棧信息

我們分析ANR問題,往往是從main log中的CPU使用情況和traces中的函數調用棧開始。所以,更新CPU的使用信息updateCpuStatsNow()方法和打印函數棧dumpStackTraces()方法,是系統報告ANR問題關鍵所在。

2.2.1 CPU的使用情況

AMS.updateCpuStatsNow()方法的實現不在這裏列出了,只需要知道更新CPU使用信息的間隔最小是5秒,即如果5秒內連續調用updateCpuStatsNow()方法,其實是沒有更新CPU使用信息的。

CPU使用信息由ProcessCpuTracker這個類維護, 每次調用ProcessCpuTracker.update()方法,就會讀取設備節點 /proc下的文件,來更新CPU使用信息,具體有以下幾個維度:

  • CPU的使用時間: 讀取 /proc/stat

    • user: 用戶進程的CPU使用時間
    • nice: 降低過優先級進程的CPU使用時間。Linux進程都有優先級,這個優先級可以進行動態調整,譬如進程初始優先級的值設爲10,運行時降低爲8,那麼,修正值-2就定義爲nice。 Android將user和nice這兩個時間歸類成user
    • sys: 內核進程的CPU使用時間
    • idle: CPU空閒的時間
    • wait: CPU等待IO的時間
    • hw irq: 硬件中斷的時間。如果外設(譬如硬盤)出現故障,需要通過硬件終端通知CPU保存現場,發生上下文切換的時間就是CPU的硬件中斷時間
    • sw irg: 軟件中斷的時間。同硬件中斷一樣,如果軟件要求CPU中斷,則上下文切換的時間就是CPU的軟件中斷時間
  • CPU負載: 讀取 /proc/loadavg, 統計最近1分鐘,5分鐘,15分鐘內,CPU的平均活動進程數。 CPU的負載可以比喻成超市收銀員負載,如果有1個人正在買單,有2個人在排隊,那麼該收銀員的負載就是3。 在收銀員工作時,不斷會有人買單完成,也不斷會有人排隊,可以在固定的時間間隔內(譬如,每隔5秒)統計一次負載,那麼,就可以統計出一段時間內的平均負載。

  • 頁錯誤信息: 進程的CPU使用率最後輸出的“faults: xxx minor/major”部分表示的是頁錯誤次數,當次數爲0時不顯示。 major是指Major Page Fault(主要頁錯誤,簡稱MPF),內核在讀取數據時會先後查找CPU的高速緩存和物理內存,如果找不到會發出一個MPF信息,請求將數據加載到內存。 minor是指Minor Page Fault(次要頁錯誤,簡稱MnPF),磁盤數據被加載到內存後,內核再次讀取時,會發出一個MnPF信息。 一個文件第一次被讀寫時會有很多的MPF,被緩存到內存後再次訪問MPF就會很少,MnPF反而變多,這是內核爲減少效率低下的磁盤I/O操作採用的緩存技術的結果。

2.2.2 函數調用棧

AMS.dumpStackTraces()方法用於打印進程的函數調用棧,該方法的主體邏輯如下:

private static void dumpStackTraces(String tracesPath, ArrayList<Integer> firstPids,
            ProcessCpuTracker processCpuTracker, SparseArray<Boolean> lastPids, String[] nativeProcs) {
    ...
    // 1. 對firstPids數組中的進程發送SIGNAL_QUIT。
    //    進程在收到SIGNAL_QUIT後,會打印函數調用棧
    int num = firstPids.size();
    for (int i = 0; i < num; i++) {
        synchronized (observer) {
            Process.sendSignal(firstPids.get(i), Process.SIGNAL_QUIT);
            observer.wait(200);  // Wait for write-close, give up after 200msec
        }
    }
    ...
    // 2. 打印Native進程的函數調用棧
    int[] pids = Process.getPidsForCommands(nativeProcs);
    if (pids != null) {
        for (int pid : pids) {
            Debug.dumpNativeBacktraceToFile(pid, tracesPath);
        }
    }
    ...
    // 3. 更新CPU的使用情況
    processCpuTracker.init();
    System.gc();
    processCpuTracker.update();
    processCpuTracker.wait(500); // measure over 1/2 second.
    processCpuTracker.update();

    // 4. 對lastPids數組中的進程發送SIGNAL_QUIT
    //    只有處於工作狀態的lastPids進程,纔會收到SIGNAL_QUIT,打印函數調用棧
    final int N = processCpuTracker.countWorkingStats();
    int numProcs = 0;
    for (int i=0; i<N && numProcs<5; i++) {
    ProcessCpuTracker.Stats stats = processCpuTracker.getWorkingStats(i);
    if (lastPids.indexOfKey(stats.pid) >= 0) {
        numProcs++;
        Process.sendSignal(stats.pid, Process.SIGNAL_QUIT);
        observer.wait(200);  // Wait for write-close, give up after 200msec
    }
}

該方法有幾個重要的邏輯(Native進程的函數調用棧此處不表):

  • 向進程發送SIGNAL_QUIT信號,進程在收到這個信號後,就會打印函數調用棧,默認輸出到 /data/anr/traces.txt 文件中, 當然也可以配置 dalvik.vm.stack-trace-file 這個系統屬性來指定輸出函數調用棧的位置

  • traces文件中包含很多進程的函數調用棧,這是由firstPids和lastPids數組控制的,在最終的traces文件中,firstPids中的進程是先打印的, 而且當前發生ANR的進程又是排在firstPids的第一個,所以,當我們打開traces文件,第一個看到的就是當前發生ANR的應用進程

3. 問題分析方法

分析ANR問題,有三大利器:Logcat,traces和StrictMode。 在StrictMode機制一文中,我們介紹過StrictMode的實現機制以及用途,本文中不討論利用StrictMode來解決ANR問題,但各位讀者需要有這個意識。 在Watchdog機制以及問題分析一文中,我們介紹過logcat和traces這兩種日誌的用途。 分析ANR問題同Watchdog問題一樣,都需要經過日誌獲取、問題定位和場景還原三個步驟。

3.1 日誌獲取

我們在上文中分析過,ANR報告機制的重要職能就是輸出日誌, 這些日誌如何取到呢?請參見日誌獲取

3.2 問題定位

通過在event log中檢索 am_anr 關鍵字,就可以找到發生ANR的進程,譬如以下日誌:

10-16 00:48:27 820 907 I am_anr: [0,29533,com.android.systemui,1082670605,Broadcast of Intent { act=android.intent.action.TIME_TICK flg=0x50000114 (has extras) }]

表示在 10-16 00:48:27 這個時刻,PID爲 29533 進程發生了ANR,進程名是 com.android.systemui

接下來可以在system log檢索 ANR in 關鍵字,找到發生ANR前後的CPU使用情況:

10-16 00:50:10 820 907 E ActivityManager: ANR in com.android.systemui, time=130090695
10-16 00:50:10 820 907 E ActivityManager: Reason: Broadcast of Intent { act=android.intent.action.TIME_TICK flg=0x50000114 (has extras) }
10-16 00:50:10 820 907 E ActivityManager: Load: 30.4 / 22.34 / 19.94
10-16 00:50:10 820 907 E ActivityManager: Android time :[2015-10-16 00:50:05.76] [130191,266]
10-16 00:50:10 820 907 E ActivityManager: CPU usage from 6753ms to -4ms ago:
10-16 00:50:10 820 907 E ActivityManager:   47% 320/netd: 3.1% user + 44% kernel / faults: 14886 minor 3 major
10-16 00:50:10 820 907 E ActivityManager:   15% 10007/com.sohu.sohuvideo: 2.8% user + 12% kernel / faults: 1144 minor
10-16 00:50:10 820 907 E ActivityManager:   13% 10654/hif_thread: 0% user + 13% kernel
10-16 00:50:10 820 907 E ActivityManager:   11% 175/mmcqd/0: 0% user + 11% kernel
10-16 00:50:10 820 907 E ActivityManager:   5.1% 12165/app_process: 1.6% user + 3.5% kernel / faults: 9703 minor 540 major
10-16 00:50:10 820 907 E ActivityManager:   3.3% 29533/com.android.systemui: 2.6% user + 0.7% kernel / faults: 8402 minor 343 major
10-16 00:50:10 820 907 E ActivityManager:   3.2% 820/system_server: 0.8% user + 2.3% kernel / faults: 5120 minor 523 major
10-16 00:50:10 820 907 E ActivityManager:   2.5% 11817/com.netease.pomelo.push.l.messageservice_V2: 0.7% user + 1.7% kernel / faults: 7728 minor 687 major
10-16 00:50:10 820 907 E ActivityManager:   1.6% 11887/com.android.email: 0.5% user + 1% kernel / faults: 6259 minor 587 major
10-16 00:50:10 820 907 E ActivityManager:   1.4% 11854/com.android.settings: 0.7% user + 0.7% kernel / faults: 5404 minor 471 major
10-16 00:50:10 820 907 E ActivityManager:   1.4% 11869/android.process.acore: 0.7% user + 0.7% kernel / faults: 6131 minor 561 major
10-16 00:50:10 820 907 E ActivityManager:   1.3% 11860/com.tencent.mobileqq: 0.1% user + 1.1% kernel / faults: 5542 minor 470 major
...
10-16 00:50:10 820 907 E ActivityManager:  +0% 12832/cat: 0% user + 0% kernel
10-16 00:50:10 820 907 E ActivityManager:  +0% 13211/zygote64: 0% user + 0% kernel
10-16 00:50:10 820 907 E ActivityManager: 87% TOTAL: 3% user + 18% kernel + 64% iowait + 0.5% softirq

這一段日誌對於Android開發人員而言,實在太熟悉不過了,它包含的信息量巨大:

  • 發生ANR的時間。event log中,ANR的時間是 00:48:27,因爲AMS.appNotResponding()首先會打印event log,然後再打印system log, 所以,在system log中,找到ANR的時間是 00:50:10。可以從這個時間點之前的日誌中,還原ANR出現時系統的運行狀態

  • 打印ANR日誌的進程。ANR日誌都是在system_server進程的AMS線程打印的,在event log和system log中,都能看到 820 和 907, 所以system_server的PID是 802,AMS線程的TID是 907。ANR的監測機制實現在AMS線程,分析一些受系統影響的ANR,需要知道system_server進程的運行狀態

  • 發生ANR的進程ANR in關鍵字就表明了當前ANR的進程是com.android.system.ui,通過event log,知道進程的PID是 29533

  • 發生ANR的原因Reason關鍵字表明瞭當前發生ANR的原因是,處理TIME_TICK廣播消息超時。 隱含的意思是TIME_TICK是一個串行廣播消息,在 29533 的主線程中,執行BroadcastReceiver.onReceive()方法已經超過10秒

  • CPU負載Load關鍵字表明瞭最近1分鐘、5分鐘、15分鐘內的CPU負載分別是30.4、22.3、19.94。CPU最近1分鐘的負載最具參考價值,因爲ANR的超時限制基本都是1分鐘以內, 這可以近似的理解爲CPU最近1分鐘平均有30.4個任務要處理,這個負載值是比較高的

  • CPU使用統計時間段CPU usage from XX to XX ago關鍵字表明瞭這是在ANR發生之前一段時間內的CPU統計。 類似的還有CPU usage from XX to XX after關鍵字,表明是ANR發生之後一段時間內的CPU統計

  • 各進程的CPU使用率。我們以com.android.systemui進程的CPU使用率爲例,它包含以下信息:

    • 總的CPU使用率: 3.3%,其中systemui進程在用戶態的CPU使用率是2.6%,在內核態的使用率是0.7%

    • 缺頁次數fault:8402 minor表示高速緩存中的缺頁次數,343 major表示內存的缺頁次數。minor可以理解爲進程在做內存訪問,major可以理解爲進程在做IO操作。 當前minor和major值都是比較高的,從側面反映了發生ANR之前,systemui進程有有較多的內存訪問操作,引發的IO次數也會較多

    • CPU使用率前面的 “+”。部分進程的CPU使用率前面有 “+” 號,譬如cat和zygote64,表示在上一次CPU統計的時間片段內,還沒有這些進程,而這一次CPU統計的時間片段內,運行了這些進程。 類似的還有 “-” 號,表示兩次CPU統計時間片段時,這些進程消亡了

  • CPU使用匯總TOTAL關鍵字表明瞭CPU使用的彙總,87%是總的CPU使用率,其中有一項iowait表明CPU在等待IO的時間,佔到64%,說明發生ANR以前,有大量的IO操作。app_process、 system_server, com.android.systemui這幾個進程的major值都比較大,說明這些進程的IO操作較爲頻繁,從而拉昇了整個iowait的時間

信息量是如此的龐大,以致於我們都要下結論了:CPU大量的時間都在等待IO,導致systemui進程分配不到CPU時間,從而主線程處理廣播消息超時,發生了ANR。

對於一個嚴謹的開發人員而言,這種結論下得有點早,因爲還有太多的疑問:

  • systemui進程也分到了一些CPU時間(3.3%),難道BroadcastReceiver.onReceive()方法就一直無法執行嗎?

  • 爲什麼iowait的時間會這麼多,而且多個進程的major值都很高?

接下來還是需要從其他日誌中還原ANR出現的場景。

3.3 場景還原

3.3.1 第一個假設和驗證

帶着上文提出來的第一個疑問,我們先來做一個假設:如果systemui進程正在執行BroadcatReceiver.onReceive()方法,那麼從traces.txt文件中,應該可以看到主線程的函數調用棧正在執行這個方法。

接下來,我們首先從traces文件中,找到發生ANR時(00:48:27),sysemtui進程的函數調用棧信息。

----- pid 29533 at 2015-10-16 00:48:06 -----
Cmd line: com.android.systemui

DALVIK THREADS (53):
"main" prio=5 tid=1 Native
  | group="main" sCount=1 dsCount=0 obj=0x75bd5818 self=0x7f8549a000
  | sysTid=29533 nice=0 cgrp=bg_non_interactive sched=0/0 handle=0x7f894bbe58
  | state=S schedstat=( 288625433917 93454573244 903419 ) utm=20570 stm=8292 core=3 HZ=100
  | stack=0x7fdffda000-0x7fdffdc000 stackSize=8MB
  | held mutexes=
  native: #00 pc 00060b0c  /system/lib64/libc.so (__epoll_pwait+8)
  native: #01 pc 0001bb54  /system/lib64/libc.so (epoll_pwait+32)
  native: #02 pc 0001b3d8  /system/lib64/libutils.so (android::Looper::pollInner(int)+144)
  native: #03 pc 0001b75c  /system/lib64/libutils.so (android::Looper::pollOnce(int, int*, int*, void**)+76)
  native: #04 pc 000d7194  /system/lib64/libandroid_runtime.so (android::NativeMessageQueue::pollOnce(_JNIEnv*, int)+48)
  at android.os.MessageQueue.nativePollOnce(Native method)
  at android.os.MessageQueue.next(MessageQueue.java:148)
  at android.os.Looper.loop(Looper.java:151)
  at android.app.ActivityThread.main(ActivityThread.java:5718)
  at java.lang.reflect.Method.invoke!(Native method)
  at java.lang.reflect.Method.invoke(Method.java:372)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:975)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:770)

----- pid 29533 at 2015-10-16 00:48:29 -----
Cmd line: com.android.systemui

DALVIK THREADS (54):
"main" prio=5 tid=1 Blocked
  | group="main" sCount=1 dsCount=0 obj=0x75bd5818 self=0x7f8549a000
  | sysTid=29533 nice=0 cgrp=bg_non_interactive sched=0/0 handle=0x7f894bbe58
  | state=S schedstat=( 289080040422 93461978317 904874 ) utm=20599 stm=8309 core=0 HZ=100
  | stack=0x7fdffda000-0x7fdffdc000 stackSize=8MB
  | held mutexes=
  at com.mediatek.anrappmanager.MessageLogger.println(SourceFile:77)
  - waiting to lock <0x26b337a3> (a com.mediatek.anrappmanager.MessageLogger) held by thread 49
  at android.os.Looper.loop(Looper.java:195)
  at android.app.ActivityThread.main(ActivityThread.java:5718)
  at java.lang.reflect.Method.invoke!(Native method)
  at java.lang.reflect.Method.invoke(Method.java:372)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:975)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:770)
...
"Binder_5" prio=5 tid=49 Native
  | group="main" sCount=1 dsCount=0 obj=0x136760a0 self=0x7f7e453000
  | sysTid=6945 nice=0 cgrp=default sched=0/0 handle=0x7f6e3ce000
  | state=S schedstat=( 5505571091 4567508913 30743 ) utm=264 stm=286 core=4 HZ=100
  | stack=0x7f6b83f000-0x7f6b841000 stackSize=1008KB
  | held mutexes=
  native: #00 pc 00019d14  /system/lib64/libc.so (syscall+28)
  native: #01 pc 0005b5d8  /system/lib64/libaoc.so (???)
  native: #02 pc 002c6f18  /system/lib64/libaoc.so (???)
  native: #03 pc 00032c40  /system/lib64/libaoc.so (???)
  at libcore.io.Posix.getpid(Native method)
  at libcore.io.ForwardingOs.getpid(ForwardingOs.java:83)
  at android.system.Os.getpid(Os.java:176)
  at android.os.Process.myPid(Process.java:754)
  at com.mediatek.anrappmanager.MessageLogger.dump(SourceFile:219)
  - locked <0x26b337a3> (a com.mediatek.anrappmanager.MessageLogger)
  at com.mediatek.anrappmanager.ANRAppManager.dumpMessageHistory(SourceFile:65)
  at android.app.ActivityThread$ApplicationThread.dumpMessageHistory(ActivityThread.java:1302)
  at android.app.ApplicationThreadNative.onTransact(ApplicationThreadNative.java:682)
  at android.os.Binder.execTransact(Binder.java:451)

最終,我們找到systemui進程ANR時刻(00:48:27)附近的兩個函數調用棧:

  1. 在ANR發生之前(00:48:06),主線程的函數調用棧處於正常狀態:消息隊列中,循環中處理消息

  2. 在ANR發生之後2秒(00:48:29),主線程處於Blocked狀態,在等待一個被49號線程持有的鎖。而49號線程是一個Binder線程,anrappmanager正在做dump操作。

筆者分析的日誌是MTK平臺產生的,所以從函數調用棧中看到com.mediatek.anrappmanager.MessageLogger這樣的類,它是MTK在AOSP上的擴展,用於打印ANR日誌。

至此,systemui進程發生ANR的直接原因我們已經找到了,systemui進程正在打印traces,存在較長時間的IO操作,導致主線程阻塞,從而無法處理TIME_TICK廣播消息,所以發生了ANR。

要避免這種場景下的ANR,我們就需要打破主線程中Blocked的邏輯。其實本例是由於MTK在AOSP的android.os.Looper.loop()擴展了打印消息隊列的功能,該功能存在設計缺陷,會導致鎖等待的情況。

3.3.2 第二個假設和驗證

我們進一步挖掘在systemui還沒有發生ANR時,就在打印traces的原因。帶着上文提出的第二個疑問,我們來做另一個假設: iowait較高,而且多個進程的major都很高,可能是由於當前正在調用AMS.dumpStackTraces()方法,很多進程都需要將自己的函數調用棧寫到traces文件,所以IO就會較高。 如果當前正在調用AMS.dumpStackTraces()方法,那說明當時系統已經發生了異常,要麼已經有ANR發生,要麼有SNR發生

event log中,我們檢索到了另一個ANR:

10-16 00:47:58 820 907 I am_anr  : [0,10464,com.android.settings,1086864965,Input dispatching timed out (Waiting to send key event because the focused window has not finished processing all of the input events that were previously delivered to it.  Outbound queue length: 0.  Wait queue length: 1.)]

在 00:47:58 這個時刻,com.android.settings進程發生了ANR,而且ANR的時間在systemui之前(00:48:27)。這一下,我們就找到佐證了,正是因爲settings進程先發生了ANR,調用AMS.dumpStackTraces(), 從而很多進程都開始了打印traces的操作,所以系統的整個iowait比較高,大量進程的major值也比較高,systemui就在其列。在MTK邏輯的影響下,打印ANR日誌會導致主線程阻塞,從而就連帶引發了其他應用的ANR。

system log中,我們檢索到了settings進程ANR的CPU使用信息:

10-16 00:48:12 820 907 E ActivityManager: ANR in com.android.settings (com.android.settings/.SubSettings), time=130063718
10-16 00:48:12 820 907 E ActivityManager: Reason: Input dispatching timed out (Waiting to send key event because the focused window has not finished processing all of the input events that were previously delivered to it.  Outbound queue length: 0.  Wait queue length: 1.)
10-16 00:48:12 820 907 E ActivityManager: Load: 21.37 / 19.25 / 18.84
10-16 00:48:12 820 907 E ActivityManager: Android time :[2015-10-16 00:48:12.24] [130077,742]
10-16 00:48:12 820 907 E ActivityManager: CPU usage from 0ms to 7676ms later:
10-16 00:48:12 820 907 E ActivityManager:   91% 820/system_server: 16% user + 75% kernel / faults: 13192 minor 167 major
10-16 00:48:12 820 907 E ActivityManager:   3.2% 175/mmcqd/0: 0% user + 3.2% kernel
10-16 00:48:12 820 907 E ActivityManager:   2.9% 29533/com.android.systemui: 2.3% user + 0.6% kernel / faults: 1352 minor 10 major
10-16 00:48:12 820 907 E ActivityManager:   2.2% 1736/com.android.phone: 0.9% user + 1.3% kernel / faults: 1225 minor 1 major
10-16 00:48:12 820 907 E ActivityManager:   2.2% 10464/com.android.settings: 0.7% user + 1.4% kernel / faults: 2801 minor 105 major
10-16 00:48:12 820 907 E ActivityManager:   0% 1785/com.meizu.experiencedatasync: 0% user + 0% kernel / faults: 3478 minor 2 major
10-16 00:48:12 820 907 E ActivityManager:   1.8% 11333/com.meizu.media.video: 1% user + 0.7% kernel / faults: 3843 minor 89 major
10-16 00:48:12 820 907 E ActivityManager:   1.5% 332/mobile_log_d: 0.5% user + 1% kernel / faults: 94 minor 1 major
10-16 00:48:12 820 907 E ActivityManager:   1% 11306/com.meizu.media.gallery: 0.7% user + 0.2% kernel / faults: 2204 minor 55 major
...
10-16 00:48:12 820 907 E ActivityManager:  +0% 11397/sh: 0% user + 0% kernel
10-16 00:48:12 820 907 E ActivityManager:  +0% 11398/app_process: 0% user + 0% kernel
10-16 00:48:12 820 907 E ActivityManager: 29% TOTAL: 5.1% user + 15% kernel + 9.5% iowait + 0% softirq

具體的涵義我們不再贅述了,只關注一下ANR的原因:

Input dispatching timed out (Waiting to send key event because the focused window has not finished processing all of the input events that were previously delivered to it.
Outbound queue length: 0. Wait queue length: 1.)

之前對Input ANR機制的分析派上用長了,我們輕鬆知道這種ANR的原因是什麼。 Wait queue length: 1表示之前的輸入事件已經派發到Settings進程了,但Settings進程還沒有處理完畢,新來的KeyEvent事件已經等待超過了5秒,所以ANR產生了。

接下來,又需要找到Settings的traces,分析Settings主線程處理輸入事件超時的原因,我們點到爲止。

4. 總結

本文對Android ANR機制進行了深入的分析:

  • ANR的監測機制,從Service,Broadcast,InputEvent三種不同的ANR監測機制的源碼實現開始,分析了Android如何發現各類ANR。在啓動服務、派發廣播消息和輸入事件時,植入超時檢測,用於發現ANR

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

  • ANR的解決方法,通過一個案例,對ANR日誌進行了深入解讀,梳理了分析ANR問題的思路和途徑

最後,致各位讀者,從日誌出發解決ANR問題,理解ANR機制背後的實現原理,碰到再難的ANR問題也無需驚慌。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章