Android客戶端的ANR監(jiān)控方案

作者:leafjia

ANR監(jiān)控是一個(gè)非常有年代感的話題了,但是市面上的ANR監(jiān)控工具,或者并非真正意義上的ANR的監(jiān)控(而是5秒卡頓監(jiān)控);或者并不完善,監(jiān)控不到到所有的ANR。而想要得到一個(gè)完善的ANR監(jiān)控工具,必須要先了解系統(tǒng)整個(gè)ANR的流程。本文分析了ANR的主要流程,給出了一個(gè)完善的ANR監(jiān)控方案。該方案已經(jīng)在Android微信客戶端上經(jīng)過全量驗(yàn)證,穩(wěn)定地運(yùn)行了一年多的時(shí)間。

我們知道ANR流程基本都是在system_server系統(tǒng)進(jìn)程完成的,系統(tǒng)進(jìn)程的行為我們很難監(jiān)控和改變,想要監(jiān)控ANR就必須找到系統(tǒng)進(jìn)程跟我們自己的應(yīng)用進(jìn)程是否有交互,如果有,兩者交互的邊界在哪里,邊界上應(yīng)用一端的行為,才是我們比較容易能監(jiān)控到的,想要要找到這個(gè)邊界,我們就必須要了解ANR的流程。

一、ANR流程

無論ANR的來源是哪里,最終都會(huì)走到ProcessRecord中的appNotResponding,這個(gè)方法包括了ANR的主要流程,所以也比較長,我們找出一些關(guān)鍵的邏輯來分析:frameworks/base/services/core/java/com/android/server/am/ProcessRecord.java:

void appNotResponding(String activityShortComponentName, ApplicationInfo aInfo,
    String parentShortComponentName, WindowProcessController parentProcess,
    boolean aboveSystem, String annotation, boolean onlyDumpSelf) {
    
    //......

    final boolean isSilentAnr;
    synchronized (mService) {
    // PowerManager.reboot() can block for a long time, so ignore ANRs while shutting down.
    if (mService.mAtmInternal.isShuttingDown()) {
        Slog.i(TAG, "During shutdown skipping ANR: " + this + " " + annotation);
        return;
    } else if (isNotResponding()) {
        Slog.i(TAG, "Skipping duplicate ANR: " + this + " " + annotation);
        return;
    } else if (isCrashing()) {
        Slog.i(TAG, "Crashing app skipping ANR: " + this + " " + annotation);
        return;
    } else if (killedByAm) {
        Slog.i(TAG, "App already killed by AM skipping ANR: " + this + " " + annotation);
        return;
    } else if (killed) {
        Slog.i(TAG, "Skipping died app ANR: " + this + " " + annotation);
        return;
    }

    // In case we come through here for the same app before completing
    // this one, mark as anring now so we will bail out.
    setNotResponding(true);

    // Log the ANR to the event log.
    EventLog.writeEvent(EventLogTags.AM_ANR, userId, pid, processName, info.flags,
            annotation);

    // Dump thread traces as quickly as we can, starting with "interesting" processes.
    firstPids.add(pid);

    // Don't dump other PIDs if it's a background ANR or is requested to only dump self.
    isSilentAnr = isSilentAnr();

    //......
}

先是一長串if else,給出了幾種比較極端的情況,會(huì)直接return,而不會(huì)產(chǎn)生一個(gè)ANR,這些情況包括:進(jìn)程正在處于正在關(guān)閉的狀態(tài),正在crash的狀態(tài),被kill的狀態(tài),或者相同進(jìn)程已經(jīng)處在ANR的流程中。

另外很重要的一個(gè)邏輯就是判斷當(dāng)前ANR是否是一個(gè)SilentAnr,所謂“沉默的ANR”,其實(shí)就是后臺(tái)ANR,后臺(tái)ANR跟前臺(tái)ANR會(huì)有不同的表現(xiàn):前臺(tái)ANR會(huì)彈無響應(yīng)的Dialog,后臺(tái)ANR會(huì)直接殺死進(jìn)程。前后臺(tái)ANR的判斷的原則是:如果發(fā)生ANR的進(jìn)程對(duì)用戶來說是有感知的,就會(huì)被認(rèn)為是前臺(tái)ANR,否則是后臺(tái)ANR。另外,如果在開發(fā)者選項(xiàng)中勾選了“顯示后臺(tái)ANR”,那么全部ANR都會(huì)被認(rèn)為是前臺(tái)ANR。

我們繼續(xù)分析這個(gè)方法:


if (!isSilentAnr && !onlyDumpSelf) {
    int parentPid = pid;
    if (parentProcess != null && parentProcess.getPid() > 0) {
        parentPid = parentProcess.getPid();
    }
    if (parentPid != pid) firstPids.add(parentPid);

    if (MY_PID != pid && MY_PID != parentPid) firstPids.add(MY_PID);

    for (int i = getLruProcessList().size() - 1; i >= 0; i--) {
        ProcessRecord r = getLruProcessList().get(i);
        if (r != null && r.thread != null) {
            int myPid = r.pid;
            if (myPid > 0 && myPid != pid && myPid != parentPid && myPid != MY_PID) {
                if (r.isPersistent()) {
                    firstPids.add(myPid);
                    if (DEBUG_ANR) Slog.i(TAG, "Adding persistent proc: " + r);
                } else if (r.treatLikeActivity) {
                    firstPids.add(myPid);
                    if (DEBUG_ANR) Slog.i(TAG, "Adding likely IME: " + r);
                } else {
                    lastPids.put(myPid, Boolean.TRUE);
                    if (DEBUG_ANR) Slog.i(TAG, "Adding ANR proc: " + r);
                }
            }
        }
    }
}

//......

// don't dump native PIDs for background ANRs unless it is the process of interest
String[] nativeProcs = null;
if (isSilentAnr || onlyDumpSelf) {
    for (int i = 0; i < NATIVE_STACKS_OF_INTEREST.length; i++) {
        if (NATIVE_STACKS_OF_INTEREST[i].equals(processName)) {
            nativeProcs = new String[] { processName };
            break;
        }
    }
} else {
    nativeProcs = NATIVE_STACKS_OF_INTEREST;
}

int[] pids = nativeProcs == null ? null : Process.getPidsForCommands(nativeProcs);
ArrayList<Integer> nativePids = null;

if (pids != null) {
    nativePids = new ArrayList<>(pids.length);
    for (int i : pids) {
        nativePids.add(i);
    }
}

發(fā)生ANR后,為了能讓開發(fā)者知道ANR的原因,方便定位問題,會(huì)dump很多信息到ANR Trace文件里,上面的邏輯就是選擇需要dump的進(jìn)程。ANR Trace文件是包含許多進(jìn)程的Trace信息的,因?yàn)楫a(chǎn)生ANR的原因有可能是其他的進(jìn)程搶占了太多資源,或者IPC到其他進(jìn)程(尤其是系統(tǒng)進(jìn)程)的時(shí)候卡住導(dǎo)致的。

選擇需要dump的進(jìn)程是一段挺有意思邏輯,我們稍微分析下:需要被dump的進(jìn)程被分為了firstPids、nativePids以及extraPids三類:

  • firstPIds:firstPids是需要首先dump的重要進(jìn)程,發(fā)生ANR的進(jìn)程無論如何是一定要被dump的,也是首先被dump的,所以第一個(gè)被加到firstPids中。如果是SilentAnr(即后臺(tái)ANR),不用再加入任何其他的進(jìn)程。如果不是,需要進(jìn)一步添加其他的進(jìn)程:如果發(fā)生ANR的進(jìn)程不是system_server進(jìn)程的話,需要添加system_server進(jìn)程;接下來輪詢AMS維護(hù)的一個(gè)LRU的進(jìn)程List,如果最近訪問的進(jìn)程包含了persistent的進(jìn)程,或者帶有BIND_TREAT_LIKE_ACTVITY標(biāo)簽的進(jìn)程,都添加到firstPids中。

  • extraPids:LRU進(jìn)程List中的其他進(jìn)程,都會(huì)首先添加到lastPids中,然后lastPids會(huì)進(jìn)一步被選出最近CPU使用率高的進(jìn)程,進(jìn)一步組成extraPids;

  • nativePids:nativePids最為簡單,是一些固定的native的系統(tǒng)進(jìn)程,定義在WatchDog.java中。

拿到需要dump的所有進(jìn)程的pid后,AMS開始按照firstPids、nativePids、extraPids的順序dump這些進(jìn)程的堆棧:

File tracesFile = ActivityManagerService.dumpStackTraces(firstPids,
        isSilentAnr ? null : processCpuTracker, isSilentAnr ? null : lastPids,
        nativePids, tracesFileException, offsets);

這里也是我們需要重點(diǎn)分析的地方,我們繼續(xù)看這里做了什么,跟到AMS里面,

frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

public static Pair<Long, Long> dumpStackTraces(String tracesFile, ArrayList<Integer> firstPids,
        ArrayList<Integer> nativePids, ArrayList<Integer> extraPids) {

    long remainingTime = 20 * 1000;
    
    //......

    if (firstPids != null) {
        int num = firstPids.size();
        for (int i = 0; i < num; i++) {
            //......
            final long timeTaken = dumpJavaTracesTombstoned(pid, tracesFile,
                                                            remainingTime);

            remainingTime -= timeTaken;
            if (remainingTime <= 0) {
                Slog.e(TAG, "Aborting stack trace dump (current firstPid=" + pid
                        + "); deadline exceeded.");
                return firstPidStart >= 0 ? new Pair<>(firstPidStart, firstPidEnd) : null;
            }
            //......
        }
    }

    //......
}

我們首先關(guān)注到remainingTime,這是一個(gè)重要的變量,規(guī)定了我們dump所有進(jìn)程的最長時(shí)間,因?yàn)閐ump進(jìn)程所有線程的堆棧,本身就是一個(gè)重操作,何況是要dump許多進(jìn)程,所以規(guī)定了發(fā)生ANR之后,dump全部進(jìn)程的總時(shí)間不能超過20秒,如果超過了,馬上返回,確保ANR彈窗可以及時(shí)的彈出(或者被kill掉)。我們繼續(xù)跟到dumpJavaTracesTombstoned

private static long dumpJavaTracesTombstoned(int pid, String fileName, long timeoutMs) {
    final long timeStart = SystemClock.elapsedRealtime();
    boolean javaSuccess = Debug.dumpJavaBacktraceToFileTimeout(pid, fileName,
            (int) (timeoutMs / 1000));
    //......
    return SystemClock.elapsedRealtime() - timeStart;
}

再一路追到native層負(fù)責(zé)dump堆棧的system/core/debuggerd/client/debuggerd_client.cpp

bool debuggerd_trigger_dump(pid_t tid, DebuggerdDumpType dump_type, unsigned int timeout_ms, unique_fd output_fd) {
    pid_t pid = tid;
    //......

    // Send the signal.
    const int signal = (dump_type == kDebuggerdJavaBacktrace) ? SIGQUIT : BIONIC_SIGNAL_DEBUGGER;
    sigval val = {.sival_int = (dump_type == kDebuggerdNativeBacktrace) ? 1 : 0};
    if (sigqueue(pid, signal, val) != 0) {
      log_error(output_fd, errno, "failed to send signal to pid %d", pid);
      return false;
    }
    //......
    LOG(INFO) << TAG "done dumping process " << pid;
    return true;
}

來了來了!之前說的交互邊界終于找到了!這里會(huì)通過sigqueue向需要dump堆棧的進(jìn)程發(fā)送SIGQUIT信號(hào),也就是signal 3信號(hào),而發(fā)生ANR的進(jìn)程是一定會(huì)被dump的,也是第一個(gè)被dump的。這就意味著,只要我們能監(jiān)控到系統(tǒng)發(fā)送的SIGQUIT信號(hào),也許就能夠監(jiān)控到發(fā)生了ANR。

每一個(gè)應(yīng)用進(jìn)程都會(huì)有一個(gè)SignalCatcher線程,專門處理SIGQUIT,來到art/runtime/signal_catcher.cc


void* SignalCatcher::Run(void* arg) {
    //......
    // Set up mask with signals we want to handle.
    SignalSet signals;
    signals.Add(SIGQUIT);
    signals.Add(SIGUSR1);
    while (true) {
        int signal_number = signal_catcher->WaitForSignal(self, signals);
        if (signal_catcher->ShouldHalt()) {
            runtime->DetachCurrentThread();
            return nullptr;
        }
        switch (signal_number) {
            case SIGQUIT:
                  signal_catcher->HandleSigQuit();
                  break;
            case SIGUSR1:
                  signal_catcher->HandleSigUsr1();
                  break;
            default:
                  LOG(ERROR) << "Unexpected signal %d" << signal_number;
                  break;
        }
    }
}

WaitForSignal方法調(diào)用了sigwait方法,這是一個(gè)阻塞方法。這里的死循環(huán),就會(huì)一直不斷的等待監(jiān)聽SIGQUIT和SIGUSR1這兩個(gè)信號(hào)的到來。

整理一下ANR的過程:當(dāng)應(yīng)用發(fā)生ANR之后,系統(tǒng)會(huì)收集許多進(jìn)程,來dump堆棧,從而生成ANR Trace文件,收集的第一個(gè),也是一定會(huì)被收集到的進(jìn)程,就是發(fā)生ANR的進(jìn)程,接著系統(tǒng)開始向這些應(yīng)用進(jìn)程發(fā)送SIGQUIT信號(hào),應(yīng)用進(jìn)程收到SIGQUIT后開始dump堆棧。來簡單畫個(gè)示意圖:

所以,事實(shí)上進(jìn)程發(fā)生ANR的整個(gè)流程,也只有dump堆棧的行為會(huì)在發(fā)生ANR的進(jìn)程中執(zhí)行。這個(gè)過程從收到SIGQUIT開始(圈1),到使用socket寫Trace(圈2)結(jié)束,然后再繼續(xù)回到server進(jìn)程完成剩余的ANR流程。我們就在這兩個(gè)邊界上做做文章。

首先我們肯定會(huì)想到,我們能否監(jiān)聽到syste_server發(fā)送給我們的SIGQUIT信號(hào)呢?如果可以,我們就成功了一半。

二、監(jiān)控SIGQUIT信號(hào)

Linux系統(tǒng)提供了兩種監(jiān)聽信號(hào)的方法,一種是SignalCatcher線程使用的sigwait方法進(jìn)行同步、阻塞地監(jiān)聽,另一種是使用sigaction方法注冊(cè)signal handler進(jìn)行異步監(jiān)聽,我們都來試試。

2.1. sigwait

我們首先嘗試前一種方法,模仿SignalCatcher線程,做一模一樣的事情,通過一個(gè)死循環(huán)sigwait,一直監(jiān)聽SIGQUIT:

static void *mySigQuitCatcher(void* args) {
    while (true) {
        int sig;
        sigset_t sigSet;
        sigemptyset(&sigSet);
        sigaddset(&sigSet, SIGQUIT);
        sigwait(&sigSet, &sig);
        if (sig == SIGQUIT) {
            //Got SIGQUIT
        }
    }
}
pthread_t pid;
pthread_create(&pid, nullptr, mySigQuitCatcher, nullptr);
pthread_detach(pid);

這個(gè)時(shí)候就有了兩個(gè)不同的線程sigwait同一個(gè)SIGQUIT,具體會(huì)走到哪個(gè)呢,我們?cè)?em>sigwait的文檔中找到了這樣的描述(sigwait方法是由sigwaitinfo方法實(shí)現(xiàn)的):

原來當(dāng)有兩個(gè)線程通過sigwait方法監(jiān)聽同一個(gè)信號(hào)時(shí),具體是哪一個(gè)線程收到信號(hào)時(shí)不能確定的**。不確定可不行,當(dāng)然不滿足我們的需求。

3.2. Signal Handler

那我們?cè)僭囅铝硪环N方法是否可行,我們通過可以sigaction方法,建立一個(gè)Signal Handler:

void signalHandler(int sig, siginfo_t* info, void* uc) {
    if (sig == SIGQUIT) {
        //Got An ANR
    }
}

struct sigaction sa;
sa.sa_sigaction = signalHandler;
sa.sa_flags = SA_ONSTACK | SA_SIGINFO | SA_RESTART;
sigaction(SIGQUIT, &sa, nullptr);

建立了Signal Handler之后,我們發(fā)現(xiàn)在同時(shí)有sigwait和signal handler的情況下,信號(hào)沒有走到我們的signal handler而是依然被系統(tǒng)的Signal Catcher線程捕獲到了,這是什么原因呢?

原來是Android默認(rèn)把SIGQUIT設(shè)置成了BLOCKED,所以只會(huì)響應(yīng)sigwait而不會(huì)進(jìn)入到我們?cè)O(shè)置的handler方法中。我們通過pthread_sigmask或者sigprocmask把SIGQUIT設(shè)置為UNBLOCK,那么再次收到SIGQUIT時(shí),就一定會(huì)進(jìn)入到我們的handler方法中。需要這樣設(shè)置:

sigset_t sigSet;
sigemptyset(&sigSet);
sigaddset(&sigSet, SIGQUIT);
pthread_sigmask(SIG_UNBLOCK, &sigSet, nullptr);

最后需要注意,我們通過Signal Handler搶到了SIGQUIT后,原本的Signal Catcher線程中的sigwait就不再能收到SIGQUIT了,原本的dump堆棧的邏輯就無法完成了,我們?yōu)榱薃NR的整個(gè)邏輯和流程跟原來完全一致,需要在Signal Handler里面重新向Signal Catcher線程發(fā)送一個(gè)SIGQUIT


int tid = getSignalCatcherThreadId(); //遍歷/proc/[pid]目錄,找到SignalCatcher線程的tid
tgkill(getpid(), tid, SIGQUIT);

(如果缺少了重新向SignalCatcher發(fā)送SIGQUIT的步驟,AMS就一直等不到ANR進(jìn)程寫堆棧,直到20秒超時(shí)后,才會(huì)被迫中斷,而繼續(xù)之后的流程。直接的表現(xiàn)就是ANR彈窗非常慢(20秒超時(shí)時(shí)間),并且/data/anr目錄下無法正常生成完整的 ANR Trace文件。)

以上就得到了一個(gè)不改變系統(tǒng)行為的前提下,比較完善的監(jiān)控SIGQUIT信號(hào)的機(jī)制,這也是我們監(jiān)控ANR的基礎(chǔ)。

三、完善的ANR監(jiān)控方案

監(jiān)控到SIGQUIT信號(hào)并不等于就監(jiān)控到了ANR。

3.1. 誤報(bào)

充分非必要條件1:發(fā)生ANR的進(jìn)程一定會(huì)收到SIGQUIT信號(hào);但是收到SIGQUIT信號(hào)的進(jìn)程并不一定發(fā)生了ANR。

考慮下面兩種情況:

  • 其他進(jìn)程的ANR:上面提到過,發(fā)生ANR之后,發(fā)生ANR的進(jìn)程并不是唯一需要dump堆棧的進(jìn)程,系統(tǒng)會(huì)收集許多其他的進(jìn)程進(jìn)行dump,也就是說當(dāng)一個(gè)應(yīng)用發(fā)生ANR的時(shí)候,其他的應(yīng)用也有可能收到SIGQUIT信號(hào)。進(jìn)一步,我們監(jiān)控到SIGQUIT時(shí),可能是監(jiān)聽到了其他進(jìn)程產(chǎn)生的ANR****,從而產(chǎn)生誤報(bào)。

  • 非ANR發(fā)送SIGQUIT:發(fā)送SIGQUIT信號(hào)其實(shí)是很容易的一件事情,開發(fā)者和廠商都可以很容易的發(fā)送一個(gè)SIGQUIT(java層調(diào)用android.os.Process.sendSignal方法;Native層調(diào)用kill或者tgkill方法),所以我們可能會(huì)收到非ANR流程發(fā)送的SIGQUIT信號(hào),從而產(chǎn)生誤報(bào)。

怎么解決這些誤報(bào)的問題呢,我重新回到ANR流程開始的地方:


void appNotResponding(String activityShortComponentName, ApplicationInfo aInfo,
        String parentShortComponentName, WindowProcessController parentProcess,
        boolean aboveSystem, String annotation, boolean onlyDumpSelf) {
    //......
    synchronized (mService) {
        if (isSilentAnr() && !isDebugging()) {
            kill("bg anr", ApplicationExitInfo.REASON_ANR, true);
            return;
        }

        // Set the app's notResponding state, and look up the errorReportReceiver
        makeAppNotRespondingLocked(activityShortComponentName,
                annotation != null ? "ANR " + annotation : "ANR", info.toString());

        // show ANR dialog ......
    }
}

private void makeAppNotRespondingLocked(String activity, String shortMsg, String longMsg) {
    setNotResponding(true);
    // mAppErrors can be null if the AMS is constructed with injector only. This will only
    // happen in tests.
    if (mService.mAppErrors != null) {
        notRespondingReport = mService.mAppErrors.generateProcessError(this,
                ActivityManager.ProcessErrorStateInfo.NOT_RESPONDING,
                activity, shortMsg, longMsg, null);
    }
    startAppProblemLocked();
    getWindowProcessController().stopFreezingActivities();
}

在ANR彈窗前,會(huì)執(zhí)行到makeAppNotRespondingLocked方法中,在這里會(huì)給發(fā)生ANR進(jìn)程標(biāo)記一個(gè)NOT_RESPONDING的flag。而這個(gè)flag我們可以通過ActivityManager來獲取:


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();
        if (procs == null) return false;
        for (ActivityManager.ProcessErrorStateInfo proc : procs) {
            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;
}

監(jiān)控到SIGQUIT后,我們?cè)?0秒內(nèi)(20秒是ANR dump的timeout時(shí)間)不斷輪詢自己是否有NOT_RESPONDING對(duì)flag,一旦發(fā)現(xiàn)有這個(gè)flag,那么馬上就可以認(rèn)定發(fā)生了一次ANR。

(你可能會(huì)想,有這么方便的方法,監(jiān)控SIGQUIT信號(hào)不是多余的嗎?直接一個(gè)死循環(huán),不斷輪訓(xùn)這個(gè)flag不就完事了?是的,理論上確實(shí)能這么做,但是這么做過于的低效、耗電和不環(huán)保外,更關(guān)鍵的是,下面漏報(bào)的問題依然無法解決)

另外,Signal Handler回調(diào)的第二個(gè)參數(shù)siginfo_t,也包含了一些有用的信息,該結(jié)構(gòu)體的第三個(gè)字段si_code表示該信號(hào)被發(fā)送的方法,SI_USER表示信號(hào)是通過kill發(fā)送的,SI_QUEUE表示信號(hào)是通過sigqueue發(fā)送的。但在Android的ANR流程中,高版本使用的是sigqueue發(fā)送的信號(hào),某些低版本使用的是kill發(fā)送的信號(hào),并不統(tǒng)一。

而第五個(gè)字段(極少數(shù)機(jī)型上是第四個(gè)字段)si_pid表示的是發(fā)送該信號(hào)的進(jìn)程的pid,這里適用幾乎所有Android版本和機(jī)型的一個(gè)條件是:如果發(fā)送信號(hào)的進(jìn)程是自己的進(jìn)程,那么一定不是一個(gè)ANR??梢酝ㄟ^這個(gè)條件排除自己發(fā)送SIGQUIT,而導(dǎo)致誤報(bào)的情況。

3.2. 漏報(bào)

充分非必要條件2:進(jìn)程處于NOT_RESPONDING的狀態(tài)可以確認(rèn)該進(jìn)程發(fā)生了ANR。但是發(fā)生ANR的進(jìn)程并不一定會(huì)被設(shè)置為NOT_RESPONDING狀態(tài)。

考慮下面兩種情況:

  • 后臺(tái)ANR(SilentAnr):之前分析ANR流程我們可以知道,如果ANR被標(biāo)記為了后臺(tái)ANR(即SilentAnr),那么殺死進(jìn)程后就會(huì)直接return,并不會(huì)走到產(chǎn)生進(jìn)程錯(cuò)誤狀態(tài)的邏輯。這就意味著,后臺(tái)ANR沒辦法捕捉到,而后臺(tái)ANR的量同樣非常大,并且后臺(tái)ANR會(huì)直接殺死進(jìn)程,對(duì)用戶的體驗(yàn)也是非常負(fù)面的,這么大一部分ANR監(jiān)控不到,當(dāng)然是無法接受的。

  • 閃退ANR:除此之外,我們還發(fā)現(xiàn)相當(dāng)一部分機(jī)型(例如OPPO、VIVO兩家的高Android版本的機(jī)型)修改了ANR的流程,即使是發(fā)生在前臺(tái)的ANR,也并不會(huì)彈窗,而是直接殺死進(jìn)程,即閃退。這部分的機(jī)型覆蓋的用戶量也非常大。并且,確定兩家今后的新設(shè)備會(huì)一直維持這個(gè)機(jī)制。

所以我們需要一種方法,在收到SIGQUIT信號(hào)后,能夠非??焖俚膫刹槌鲎约菏遣皇且烟幱贏NR的狀態(tài),進(jìn)行快速的dump和上報(bào)。很容易想到,我們可以通過主線程是否處于卡頓狀態(tài)來判斷。那么怎么最快速的知道主線程是不是卡住了呢?上一篇文章中,分析Sync Barrier泄漏問題時(shí),我們反射過主線程Looper的mMessage對(duì)象,該對(duì)象的when變量,表示的就是當(dāng)前正在處理的消息入隊(duì)的時(shí)間,我們可以通過when變量減去當(dāng)前時(shí)間,得到的就是等待時(shí)間,如果等待時(shí)間過長,就說明主線程是處于卡住的狀態(tài),這時(shí)候收到SIGQUIT信號(hào)基本上就可以認(rèn)為的確發(fā)生了一次ANR:

private static boolean isMainThreadStuck(){
    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) {
            long when = mMessage.getWhen();
            if(when == 0) {
                return false;
            }
            long time = when - SystemClock.uptimeMillis();
            long timeThreshold = BACKGROUND_MSG_THRESHOLD;
            if (foreground) {
                timeThreshold = FOREGROUND_MSG_THRESHOLD;
            }
            return time < timeThreshold;
        }
    } catch (Exception e){
        return false;
    }
    return false;
}

我們通過上面幾種機(jī)制來綜合判斷收到SIGQUIT信號(hào)后,是否真的發(fā)生了一次ANR,最大程度地減少誤報(bào)和漏報(bào),才是一個(gè)比較完善的監(jiān)控方案。

3.3. 額外收獲:獲取ANR Trace

回到之前畫的ANR流程示意圖,Signal Catcher線程寫Trace(圈2)也是一個(gè)邊界,并且是通過socket的write方法來寫Trace的,如果我們能夠hook到這里的write,我們甚至就可以拿到系統(tǒng)dump的ANR Trace內(nèi)容。這個(gè)內(nèi)容非常全面,包括了所有線程的各種狀態(tài)、鎖和堆棧(包括native堆棧),對(duì)于我們排查問題十分有用,尤其是一些native問題和死鎖等問題。Native Hook我們采用PLT Hook 方案,這種方案在微信上已經(jīng)被驗(yàn)證了其穩(wěn)定性是可控的。


int (*original_connect)(int __fd, const struct sockaddr* __addr, socklen_t __addr_length);
int my_connect(int __fd, const struct sockaddr* __addr, socklen_t __addr_length) {
    if (strcmp(__addr->sa_data, "/dev/socket/tombstoned_java_trace") == 0) {
        isTraceWrite = true;
        signalCatcherTid = gettid();
    }
    return original_connect(__fd, __addr, __addr_length);
}

int (*original_open)(const char *pathname, int flags, mode_t mode);
int my_open(const char *pathname, int flags, mode_t mode) {
    if (strcmp(pathname, "/data/anr/traces.txt") == 0) {
        isTraceWrite = true;
        signalCatcherTid = gettid();
    }
    return original_open(pathname, flags, mode);
}

ssize_t (*original_write)(int fd, const void* const __pass_object_size0 buf, size_t count);
ssize_t my_write(int fd, const void* const buf, size_t count) {
    if(isTraceWrite && signalCatcherTid == gettid()) {
        isTraceWrite = false;
        signalCatcherTid = 0;
        char *content = (char *) buf;
        printAnrTrace(content);
    }
    return original_write(fd, buf, count);
}

void hookAnrTraceWrite() {
    int apiLevel = getApiLevel();
    if (apiLevel < 19) {
        return;
    }
    if (apiLevel >= 27) {
        plt_hook("libcutils.so", "connect", (void *) my_connect, (void **) (&original_connect));
    } else {
        plt_hook("libart.so", "open", (void *) my_open, (void **) (&original_open));
    }

    if (apiLevel >= 30 || apiLevel == 25 || apiLevel ==24) {
        plt_hook("libc.so", "write", (void *) my_write, (void **) (&original_write));
    } else if (apiLevel == 29) {
        plt_hook("libbase.so", "write", (void *) my_write, (void **) (&original_write));
    } else {
        plt_hook("libart.so", "write", (void *) my_write, (void **) (&original_write));
    }
}

其中有幾點(diǎn)需要注意:

  • 只Hook ANR流程:有些情況下,基礎(chǔ)庫中的connect/open/write方法可能調(diào)用的比較頻繁,我們需要把hook的影響降到最低。所以我們只會(huì)在接收到SIGQUIT信號(hào)后(重新發(fā)送SIGQUIT信號(hào)給Signal Catcher前)進(jìn)行hook,ANR流程結(jié)束后再unhook。

  • 只處理Signal Catcher線程open/connect后的第一次write:除了Signal Catcher線程中的dump trace的流程,其他地方調(diào)用的write方法我們并不關(guān)心,并不需要處理。例如,dump trace的流程會(huì)在在write方法前,系統(tǒng)會(huì)先使用connet方法鏈接一個(gè)path為“/dev/socket/tombstoned_java_trace”的socket,我們可以hook connect方法,拿到這個(gè)socket的name,我們只處理connect這個(gè)socket后,相同線程(即Signal Catcher線程)的第一次write,這次write的內(nèi)容才是我們唯一關(guān)心的。

  • Hook點(diǎn)因API Level而不同:需要hook的write方法在不同的Android版本中,所在的so也不盡相同,不同API Level需要分別處理,hook不同的so和方法。目前這個(gè)方案在API 18以上都測(cè)試過可行。

這個(gè)Hook Trace的方案,不僅僅可以用來查ANR問題,任何時(shí)候我們都可以手動(dòng)向自己發(fā)送一個(gè)SIGQUIT信號(hào),從而hook到當(dāng)時(shí)的Trace。Trace的內(nèi)容對(duì)于我們排查線程死鎖,線程異常,耗電等問題都非常有幫助。

這樣我們就得到了一個(gè)完善的ANR監(jiān)控方案,這套方案在微信上平穩(wěn)運(yùn)行了很長一段時(shí)間,給我們?cè)u(píng)估和優(yōu)化微信Android客戶端的質(zhì)量提供了非常重要根據(jù)和方向。

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

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

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