作者: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ù)和方向。