記一個主線程卡死卻沒有ANR的BUG 問題定位與分析 ANR原理 感想

今天測試報了個BUG,分析了一波順利解決問題。但是感覺中間的一些思路、技巧和知識點比較有意思,所以記錄下來。

問題定位與分析

首先這個問題是是個概率性問題,在壓測整機復位功能的時候出現的。我負責的某個服務在開機的時候會自啓動,測試發現某一次復位完成開機之後功能沒有辦法正常使用,立馬叫我過去看。

  1. 首先我到的時候現場是還在的,由於這是個Service,ui上看不出異常。所以adb 連接上機器之後使用PS命令查看進程,發現服務的進程是存在的
  2. 其次查看log,沒有發現任何的異常打印或者奔潰重啓的痕跡
  3. 接着查找關鍵日誌發現異常,這個服務在子線程做完一些初始化操作之後會同步回主線程打開功能:
Log.d(TAG, "child thread finish");
mHandler.sendEmptyMessage(MSG_START_FUNCTION);

子線程的打印找到了,而且它的下一行就是用Handler發送Message,但是主線程接的打印沒有找到。

由於這部分的代碼十分簡單,不存在什麼bug,除非Handler機制出問題了。由於我們的機器還在研發階段,系統哥調試的時候不小心改出什麼奇怪的問題也是可能的,但是我們不能一上來就這麼想,要不然把問題轉給系統哥也會一臉懵逼無從入手。

由於Handler的Message是逐個執行的,所以如果某個Message堵死了也會造成後面的Message沒法處理。由於這次是主線程的Handler,如果我們的主線程卡死了也會出現這種問題。

但是主線程卡死的話已經十幾分鍾過去了也沒有出現ANR,/data/anr/下面也是空的。不過我們可以使用kill -3 <pid>命令強制輸出trace文件,查看應用當前所有線程的調用棧。然後分析主線程現在是個啥情況:

"main" prio=5 tid=1 Waiting
  | group="main" sCount=1 dsCount=0 flags=1 obj=0x7137cc28 self=0xe3f82a10
  | sysTid=1208 nice=0 cgrp=default sched=0/0 handle=0xf09e6470
  | state=S schedstat=( 456814323 745320630 635 ) utm=40 stm=5 core=0 HZ=100
  | stack=0xff1c8000-0xff1ca000 stackSize=8192KB
  | held mutexes=
  at java.lang.Object.wait(Native method)
  - waiting on <0x017f64da> (a java.lang.Object)
  at java.lang.Object.wait(Object.java:442)
  at java.lang.Object.wait(Object.java:568)
  at h.a.a.a.a.l.q.f(:4)
  - locked <0x017f64da> (a java.lang.Object)
  at h.a.a.a.a.l.q.e(:2)
  at d.d.a.d.f.d.b(:3)
  at d.d.a.d.f.a.run(lambda:-1)
  at android.os.Handler.handleCallback(Handler.java:938)
  at android.os.Handler.dispatchMessage(Handler.java:99)
  at android.os.Looper.loop(Looper.java:223)
  at android.app.ActivityThread.main(ActivityThread.java:7666)
  at java.lang.reflect.Method.invoke(Native method)
  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:952)

很給力,立馬就驗證了問題,主線程果然卡死在Object.wait了。

但是代碼裏面搜索了一圈並沒有直接使用這個wait方法,倒是有個第三方庫的類似的操作可能會用到它。由於之前一直沒報過這種問題,應該是小概率實際所以我們必須給出實錘並且解決,要不然問題的迴歸比較難。

代碼被混淆了,雖然我們可以用mapping.txt文件來還原,但是由於這個項目的配套還不成熟,版本號機制都還沒有加上去,所以找到對應的版本和mapping.txt文件比較困難。

於是我選擇將apk從機器裏面adb pull扣出來用jadx反編譯找到h.a.a.a.a.l.q.f這個方法看看:

可以看到一些字符串和大概的代碼邏輯。和之前猜測的第三方庫做對比,發現的確我們的猜測是正確的,然後一步步對應整理回整個堆棧。發現的確是第三方庫的某個方法wait一直阻塞住了主線程。這應該是第三方庫的bug,幸好它有個重載方法可以傳入超時時間,所以我們添加了個3秒的超時時間,超時之後重試去解決問題。另外在主線程等待也不是個好的習慣,我們可以將它挪到子線程中。

ANR原理

雖然問題解決了,但是其實還有些知識點比較有意思值得去深究。我們都知道不能在主線程不能做耗時操作,要不然會ANR。但是這個問題主線程都阻塞十幾分鍾了,就算我們的是Service也應該最多200s後(後臺服務)就會ANR,爲啥就是沒有ANR呢?

我恢復堆棧之後發現,這個wait的阻塞是在Application.onCreate的時候調用的,也就是說Application.onCreate的卡頓並不會導致ANR。

我們來回顧下ANR的4種類型:

1. KeyDispatchTimeout : input事件在5S內沒有處理完成發生ANR

2. ServiceTimeout : bind,create,start,unbind等操作,前臺Service在20s內,後臺Service在200s內沒有處理完成發生ANR

3. BroadcastTimeout : BroadcastReceiver onReceiver處理事務時前臺廣播在10S內,後臺廣播在60s內. 沒有處理完成發生ANR

4. ProcessContentProviderPublishTimedOutLocked : ContentProvider publish在10s內沒有處理完成發 生ANR

的確上面Service、Broadcast、ContentProvider的ANR原因都是對應組件的生命週期回調超時,他們ANR的計算並沒有包括Application.onCreate因爲這個回調是進程的初始化,並不在四大組件中。

另外一個知識點是並沒有Activity生命週期的ANR,也就是說我們在Activity的onCreate、onStart這些生命週期中阻塞並不會造成ANR。

Activity的ANR都是input事件例如按鍵和觸摸消息處理耗時導致的。

定時炸彈機制

ServiceTimeout、BroadcastTimeout、ProcessContentProviderPublishTimedOutLocked的原理都是類似的

  1. 在處理前使用Handler.sendMessageDelayed發送一個ANR消息
  2. 在處理完成之後使用Handler.removeMessages刪除ANR消息

這裏可以類比成一個定時炸彈,在處理前埋下定時炸彈,只要沒有再規定的時間內完成處理並且拆除炸彈,就會爆炸。

我們這裏只舉一個Service的例子。在AMS裏面調用Service.onCreate之前會sendMessageDelayed一個SERVICE_TIMEOUT_MSG的Message:

// AMS start service核心代碼
private final void realStartServiceLocked(ServiceRecord r, ProcessRecord app, boolean execInFg) throws RemoteException {
    ...
    // 在bumpServiceExecutingLocked裏面會發送SERVICE_TIMEOUT_MSG
    bumpServiceExecutingLocked(r, execInFg, "create");
    ...
    // 異步調用Service.onCreate
    app.thread.scheduleCreateService(r, r.serviceInfo,
                    mAm.compatibilityInfoForPackage(r.serviceInfo.applicationInfo),
                    app.getReportedProcState());
    ...
}

// 下面的代碼追蹤bumpServiceExecutingLocked是如何發生SERVICE_TIMEOUT_MSG的
private final void bumpServiceExecutingLocked(ServiceRecord r, boolean fg, String why) {
    ...
    scheduleServiceTimeoutLocked(r.app);
    ...
}

private final void bumpServiceExecutingLocked(ServiceRecord r, boolean fg, String why) {
    ...
    scheduleServiceTimeoutLocked(r.app);
    ...
}

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;
    mAm.mHandler.sendMessageDelayed(msg,
            proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
}

而在ActivityThread裏面Service.onCreate調用完成之後會通知AMS:

private void handleCreateService(CreateServiceData data) {
    ...
    // 創建service
    service = packageInfo.getAppFactory().instantiateService(cl, data.info.name, data.intent);
    // 調用onCreate生命週期
    service.onCreate();
    ...
    // 告訴AMS,Service.onCreate已經調用完成
    ActivityManager.getService().serviceDoneExecuting(
                        data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);
  ...
}

AMS就會再serviceDoneExecutingLocked裏面拆炸彈:

private void serviceDoneExecutingLocked(ServiceRecord r, boolean inDestroying, boolean finishing) {
    ...
    // 刪除SERVICE_TIMEOUT_MSG
    mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_TIMEOUT_MSG, r.app);
    ...
}

這種機制打個比方就是歹徒(AMS)在你家裝了個定時炸彈,然後威脅你去幹一件事,你必須在規定時間內完成然後告訴他停止計時,要不然就會把你家炸上天(ANR)

KeyDispatchTimeout原理

Activity的ANR並不是通過上面所說的埋定時炸彈的方式實現的,它有另外一套邏輯。

前面我們也有說Activity的生命週期是不會觸發ANR的,它的ANR實際上是在處理input事件的時候產生的。例如在按鍵消息或者觸摸消息處理裏面耗時太久。

input事件的底層分發邏輯以前寫過兩篇博客感興趣的同學可以詳細瞭解下。我們這篇來補充上input事件分發的ANR檢測原理。

相關代碼在native層的InputDispatcher.cpp裏面,每個input事件都會喚醒Dispatcher線程進行分發處理,我們以按鍵消息爲例:

void InputDispatcher::dispatchOnce() {
    ...
    dispatchOnceInnerLocked(&nextWakeupTime);
    ...
}

void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
    ...
    // Ready to start a new event.
  // If we don't already have a pending event, go grab one.
    if (! mPendingEvent) {
        ...
        resetANRTimeoutsLocked();
    }
    ...

    switch (mPendingEvent->type) {
        ...
        case EventEntry::TYPE_KEY: {
            ...
            done = dispatchKeyLocked(currentTime, typedEntry, &dropReason, nextWakeupTime);
            ...
        }
        ...
    }
    ...
}

假設我們的應用接收到了它的第一個input事件KEY_DOWN。可以看到dispatchOnceInnerLocked裏面判斷如果是一個新的事件,就調用resetANRTimeoutsLocked清除ANR的標記,然後使用dispatchKeyLocked進行分發。

resetANRTimeoutsLocked裏面最重要的一步是將mInputTargetWaitCause設置成INPUT_TARGET_WAIT_CAUSE_NONE:

void InputDispatcher::resetANRTimeoutsLocked() {
    ...
    mInputTargetWaitCause = INPUT_TARGET_WAIT_CAUSE_NONE;
    ...
}

dispatchKeyLocked裏面回去獲取當前的焦點windows分發按鍵消息:

bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, KeyEntry* entry,
        DropReason* dropReason, nsecs_t* nextWakeupTime) {
    ...
    int32_t injectionResult = findFocusedWindowTargetsLocked(currentTime,
            entry, inputTargets, nextWakeupTime);
    ...
}

int32_t InputDispatcher::findFocusedWindowTargetsLocked(nsecs_t currentTime,
        const EventEntry* entry, Vector<InputTarget>& inputTargets, nsecs_t* nextWakeupTime) {
    ...
    reason = checkWindowReadyForMoreInputLocked(currentTime,
            mFocusedWindowHandle, entry, "focused");
    if (!reason.isEmpty()) {
        injectionResult = handleTargetsNotReadyLocked(currentTime, entry,
                mFocusedApplicationHandle, mFocusedWindowHandle, nextWakeupTime, reason.string());
        ...
    }
    ...
}

由於是一個新的事件,所以windows沒有正在處理的消息。checkWindowReadyForMoreInputLocked拿到的reson是empty的,不會進入handleTargetsNotReadyLocked,而是正常向這個window分發。

如果應用處理KEY_DOWN卡死了,那麼在用戶擡起手指觸發KEY_UP事件的時候mPendingEvent則不爲NULL,不會清除ANR標記,而且checkWindowReadyForMoreInputLocked返回的reason不是empty,就會進入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 (mInputTargetWaitCause != INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY) {
        ...
        mInputTargetWaitCause = INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY;
        ...
        mInputTargetWaitTimeoutTime = currentTime + timeout;
        ...
    }
    ...
    if (currentTime >= mInputTargetWaitTimeoutTime) {
        onANRLocked(currentTime, applicationHandle, windowHandle,
                entry->eventTime, mInputTargetWaitStartTime, reason);
        ...
    } else {
        *nextWakeupTime = mInputTargetWaitTimeoutTime;
        ...
    }
 }

我們看到這個方法裏面判斷mInputTargetWaitCause不是INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY(因爲KEY_DOWN已經在resetANRTimeoutsLocked裏面將它設置成INPUT_TARGET_WAIT_CAUSE_NONE了),所以會進入if裏面設置mInputTargetWaitTimeoutTime和mInputTargetWaitCause。

後面的"currentTime >= mInputTargetWaitTimeoutTime"判斷因爲是剛設置的mInputTargetWaitTimeoutTime所以不會進入,而是會去到else裏面設置nextWakeupTime,然後線程會睡眠。也就是說這個KEY_UP時間會被延遲timeout時間再執行。

等時間到了線程被喚醒的時候mInputTargetWaitCause,已經是INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY了,所以不會被修改,然後"currentTime >= mInputTargetWaitTimeoutTime"判斷會成功進入onANRLocked觸發應用的ANR。

簡單來講就是KEY_UP事件到來的時候發現之前上個事件還沒有處理完,於是延遲5s再來看看,如果這個時候上個事件依然沒有處理完,則觸發ANR。

這種機制有個特點就是假設你在KEY_UP裏面卡死了,但是界面是沒有任何動畫,也不去觸發input事件。那麼雖然主線程卡死了,但是無論過多久都不會報ANR。如果這個時候你再去觸發input事件(例如觸摸或者按鍵),就會發現過多5秒就出現ANR了。

同樣打個比方這種機制就像一個暴躁的恐怖分子(Input事件)去找神父(FocusWindow)懺悔,如果發現神父已經在接客了,就會過一會再來看看,如果到時候神父還是沒空,就會引爆炸彈一了百了(ANR)。

感想

隨着年紀的增長,腦子就像個長時間運行的硬盤,塞滿了各種有用的沒用的東西。加載速度和檢索的命中率越來越低。就像是以前明明有去專門看過ANR的原理,但是看到這個問題我的第一反應也是主線程不可能卡死要不然就ANR了。所以除了各種死記硬背的八股文知識,我認爲更應該重視調試技巧和解決問題能力,這纔是老年程序員的核心競爭力。

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