[ANR監(jiān)控] ANR捕獲,這些要點(diǎn)你必須知道

大家都知道,當(dāng)發(fā)生ANR后,App會彈窗提示”應(yīng)用失去響應(yīng),是否重啟“,然后系統(tǒng)會dump一份trace文件,存在data/anr目錄下。

普通應(yīng)用如何監(jiān)控ANR的發(fā)生呢?

這個時候,系統(tǒng)肯定是知道ANR發(fā)生了,所以像Console和Firebase這些工具都能很好的拿到ANR發(fā)生的時間和trace文件的內(nèi)容。

但是,作為面向普通應(yīng)用的監(jiān)控sdk,很多系統(tǒng)應(yīng)用有的權(quán)限都沒有,我們怎么才能判斷ANR的發(fā)生呢?另外高版本的Android系統(tǒng),限制了普通應(yīng)用讀取trace文件的權(quán)限,我們又如何拿到ANR發(fā)生時dump出來的trace文件呢?

ANR捕獲的基本原理

發(fā)生ANR的時候,系統(tǒng)的system_server進(jìn)程會發(fā)送SIGQUIT信號給一些進(jìn)程,包括發(fā)生ANR的進(jìn)程、占用系統(tǒng)資源較多的進(jìn)程,讓這些進(jìn)程進(jìn)行dump操作。

我們可以通過在應(yīng)用內(nèi)攔截這個信號,來判斷當(dāng)前進(jìn)程是否需要dump。但是收到信號并不表示當(dāng)前進(jìn)程一定發(fā)生了ANR,也可能是其他進(jìn)程發(fā)生了ANR,剛好我們的進(jìn)程運(yùn)行在后臺,且資源消耗比較多,system_server也發(fā)送信號讓我們進(jìn)程去做dump。所以光憑這個邏輯,無法判斷是否是當(dāng)前進(jìn)程發(fā)生的ANR,還需要加一些其他的判斷條件。接下來就讓我們從Matrix的源碼角度,看看業(yè)界通用的比較成熟的通用方案吧。

監(jiān)聽SIGQUIT信號

  1. 首先Matrix在AnrDumper方法里,將sigquit設(shè)置成SIG_UNBLOCK。

因?yàn)锳ndroid默認(rèn)把SIGQUIT信號設(shè)置成BLOCKED,所以只會響應(yīng)sigwait,而不會進(jìn)入我們設(shè)置的handler中。所以需要通過pthread_sigmaskSIGQUIT信號設(shè)置成UNBLOCK,才能進(jìn)入我們的handler方法。

AnrDumper::AnrDumper(const char* anrTraceFile, const char* printTraceFile) {
    // must unblock SIGQUIT, otherwise the signal handler can not capture SIGQUIT
    mAnrTraceFile = anrTraceFile;
    mPrintTraceFile = printTraceFile;
    sigset_t sigSet;
    sigemptyset(&sigSet);
    sigaddset(&sigSet, SIGQUIT);
    pthread_sigmask(SIG_UNBLOCK, &sigSet , &old_sigSet);
}
  1. 然后通過sigaction,將方法地址設(shè)置成我們的signalHandler。
bool SignalHandler::installHandlersLocked() {
    // 構(gòu)造一個sigaction,將方法地址設(shè)置成我們的signalHandler
    struct sigaction sa{};
    sa.sa_sigaction = signalHandler;
    sa.sa_flags = SA_ONSTACK | SA_SIGINFO | SA_RESTART;
    // 設(shè)置一個新的sigaction
    if (sigaction(TARGET_SIG, &sa, nullptr) == -1) {
        return false;
    }
    sHandlerInstalled = true;
    return true;
}
  1. 當(dāng)系統(tǒng)向進(jìn)程發(fā)送SIGQUIT信號時,會進(jìn)入我們的signalHandler方法,可以在這里可以對信號進(jìn)行進(jìn)一步的處理。Matrix是直接交給AnrDumperhandleSignal方法處理。
void SignalHandler::signalHandler(int sig, siginfo_t* info, void* uc) {
    std::unique_lock<std::mutex> lock(sHandlerStackMutex);
    for (auto it = sHandlerStack->rbegin(); it != sHandlerStack->rend(); ++it) {
        (*it)->handleSignal(sig, info, uc);
    }
    lock.unlock();
}
  1. 在處理完之后,將SIGQUIT重新發(fā)成系統(tǒng)的SignalCatcher處理。
static void sendSigToSignalCatcher() {
    int tid = getSignalCatcherThreadId();
    syscall(SYS_tgkill, getpid(), tid, SIGQUIT);
}

判斷是否真的發(fā)生ANR

收到SIGQUIT只能說明進(jìn)程要處理dump,并不能說明當(dāng)前應(yīng)用程序發(fā)生了ANR,如果直接上報ANR,會造成多報。所以還需要加更多的判斷條件,來判斷當(dāng)前進(jìn)程是否真正發(fā)生ANR。

  1. 當(dāng)前程序是否有NOT_RESPONDING標(biāo)記

如果應(yīng)用發(fā)生了ANR,AMS會給進(jìn)程加一個NOT_RESPONDING的標(biāo)記,同時生成shortMsglongMsg。如果有這個標(biāo)記,可以馬上判斷當(dāng)前進(jìn)程發(fā)生了一次ANR。

但是用NOT_RESPONDING標(biāo)記來判斷ANR,也有幾個缺陷:

  • 生成比較慢,需要輪詢?nèi)ゲ?,如果用戶提前殺掉app,可能導(dǎo)致上報失敗
  • 無法監(jiān)控后臺ANR,因?yàn)楹笈_ANR,除非了【開發(fā)者選項(xiàng)】中打開【顯示所有“應(yīng)用程序無響應(yīng)】,否則App不會進(jìn)入NOT_RESPONDING狀態(tài)。

Matrix的做法是開啟一個子線程,去循環(huán)查是否有NOT_RESPONDING標(biāo)記,500ms查一次,一共查20s。

new Thread(new Runnable() {
                @Override
                public void run() {
                    checkErrorStateCycle(isSigQuit);
                }
            }, CHECK_ANR_STATE_THREAD_NAME).start();

循環(huán)檢查進(jìn)程的Error State。

    private static void checkErrorStateCycle(boolean isSigQuit) {
        int checkErrorStateCount = 0;
        while (checkErrorStateCount < CHECK_ERROR_STATE_COUNT) {
            try {
                checkErrorStateCount++;
                boolean myAnr = checkErrorState();
                //  如果有NOT_RESPONDING標(biāo)記,馬上上報
                if (myAnr) {
                    report(true, isSigQuit);
                    break;
                }
                Thread.sleep(CHECK_ERROR_STATE_INTERVAL);
            } catch (Throwable t) {
                MatrixLog.e(TAG, "checkErrorStateCycle error, e : " + t.getMessage());
                break;
            }
        }
    }

單詞檢查的邏輯,通過getProcessInErrorState獲取。

    private static boolean checkErrorState() {
        try {
            Application application =
                    sApplication == null ? Matrix.with().getApplication() : sApplication;
            ActivityManager am = (ActivityManager) application
                    .getSystemService(Context.ACTIVITY_SERVICE);
            List<ActivityManager.ProcessErrorStateInfo> procs = am.getProcessesInErrorState();
            for (ActivityManager.ProcessErrorStateInfo proc : procs) {
                if (proc.uid != android.os.Process.myUid()
                        && proc.condition == ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING) {
                    MatrixLog.i(TAG, "maybe received other apps ANR signal");
                    return false;
                }
                // 當(dāng)前進(jìn)程,且狀態(tài)是NOT_RESPONDING,才返回true
                if (proc.pid != android.os.Process.myPid()) continue;
                if (proc.condition != ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING) {
                    continue;
                }
                return true;
            }
            return false;
        } catch (Throwable t) {
            MatrixLog.e(TAG, "[checkErrorState] error : %s", t.getMessage());
        }
        return false;
    }
  1. 主線程是否被block

通過反射,可以獲取主線程Looper中的消息頭mMessages。因?yàn)?code>MessageQueue中的消息是根據(jù)Message.when的時間升序排列的,所以反射獲取的消息頭就是第一個待處理的消息。

判斷主線程是否被block的方法:判斷當(dāng)前時間和when的差值。

  • 前臺:如果差值大于2s,算block
  • 后臺:差值大于10s,算block
    private static final long FOREGROUND_MSG_THRESHOLD = -2000;
    private static final long BACKGROUND_MSG_THRESHOLD = -10000;

    @RequiresApi(api = Build.VERSION_CODES.M)
    private static boolean isMainThreadBlocked() {
        try {
            MessageQueue mainQueue = Looper.getMainLooper().getQueue();
            Field field = mainQueue.getClass().getDeclaredField("mMessages");
            field.setAccessible(true);
            final Message mMessage = (Message) field.get(mainQueue);
            if (mMessage != null) {
                anrMessageString = mMessage.toString();
                long when = mMessage.getWhen();
                if (when == 0) {
                    return false;
                }
                long time = when - SystemClock.uptimeMillis();
                anrMessageWhen = time;
                long timeThreshold = BACKGROUND_MSG_THRESHOLD;
                if (currentForeground) {
                    timeThreshold = FOREGROUND_MSG_THRESHOLD;
                }
                return time < timeThreshold;
            } else {
                MatrixLog.i(TAG, "mMessage is null");
            }
        } catch (Exception e) {
            return false;
        }
        return false;
    }

主動模擬SIGQUIT信號

另外,我們自己進(jìn)程也可以手動觸發(fā)一個SIGQUIT信號,來獲取trace文件里額外的信息。比如在發(fā)生卡頓的時候,也可以使用SIGQUIT來讓我們進(jìn)程dump一個當(dāng)前的堆棧。

不過這個方法比較重,因?yàn)閐ump堆棧的時候,需要暫停所有的線程,會影響程序的運(yùn)行效率。所以一般不會在線上輕易使用,可以在線下監(jiān)控的時候用。

static void nativePrintTrace() {
    fromMyPrintTrace = true;
    kill(getpid(), SIGQUIT);
}

總結(jié)

綜上所述,判斷當(dāng)前進(jìn)程是否發(fā)生ANR的業(yè)界通用做法如下:

  • 通過攔截SIGQUIT來判斷系統(tǒng)是否向進(jìn)程發(fā)送信號
  • 收到SIGQUIT后,判斷主線程是否block,如果主線程block,上報ANR
  • 循環(huán)獲取App ErrorState,如果狀態(tài)變成NOT_RESPONDING,上報ANR

下一篇文章會講,如何獲取系統(tǒng)dump的trace文件,敬請期待。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容