1. 程式人生 > 其它 >手把手教你高效監控ANR

手把手教你高效監控ANR

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

我們知道ANR流程基本都是在system_server系統程序完成的,系統程序的行為我們很難監控和改變,想要監控ANR就必須找到系統程序跟我們自己的應用程序是否有互動,如果有,兩者互動的邊界在哪裡,邊界上應用一端的行為,才是我們比較容易能監控到的,想要要找到這個邊界,我們就必須要了解ANR的流程。

一、ANR流程

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

void appNotResponding(String activityShortComponentName, ApplicationInfo aInfo,

先是一長串if else,給出了幾種比較極端的情況,會直接return,而不會產生一個ANR,這些情況包括:程序正在處於正在關閉的狀態,正在crash的狀態,被kill的狀態,或者相同程序已經處在ANR的流程中。

另外很重要的一個邏輯就是判斷當前ANR是否是一個SilentAnr,所謂“沉默的ANR”,其實就是後臺ANR,後臺ANR跟前臺ANR會有不同的表現:前臺ANR會彈無響應的Dialog,後臺ANR會直接殺死程序。前後臺ANR的判斷的原則是:如果發生ANR的程序對使用者來說是有感知的,就會被認為是前臺ANR,否則是後臺ANR。另外,如果在開發者選項中勾選了“顯示後臺ANR”,那麼全部ANR都會被認為是前臺ANR。

我們繼續分析這個方法:

if (!isSilentAnr && !onlyDumpSelf) {

發生ANR後,為了能讓開發者知道ANR的原因,方便定位問題,會dump很多資訊到ANR Trace檔案裡,上面的邏輯就是選擇需要dump的程序。ANR Trace檔案是包含許多程序的Trace資訊的,因為產生ANR的原因有可能是其他的程序搶佔了太多資源,或者IPC到其他程序(尤其是系統程序)的時候卡住導致的。

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

  • firstPIds:firstPids是需要首先dump的重要程序,發生ANR的程序無論如何是一定要被dump的,也是首先被dump的,所以第一個被加到firstPids中。如果是SilentAnr(即後臺ANR),不用再加入任何其他的程序。如果不是,需要進一步新增其他的程序:如果發生ANR的程序不是system_server程序的話,需要新增system_server程序;接下來輪詢AMS維護的一個LRU的程序List,如果最近訪問的程序包含了persistent的程序,或者帶有BIND_TREAT_LIKE_ACTVITY標籤的程序,都新增到firstPids中。

  • extraPids:LRU程序List中的其他程序,都會首先新增到lastPids中,然後lastPids會進一步被選出最近CPU使用率高的程序,進一步組成extraPids;

  • nativePids:nativePids最為簡單,是一些固定的native的系統程序,定義在WatchDog.java中。

拿到需要dump的所有程序的pid後,AMS開始按照firstPids、nativePids、extraPids的順序dump這些程序的堆疊:

File tracesFile = ActivityManagerService.dumpStackTraces(firstPids,

這裡也是我們需要重點分析的地方,我們繼續看這裡做了什麼,跟到AMS裡面,

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

public static Pair<Long, Long> dumpStackTraces(String tracesFile, ArrayList<Integer> firstPids,

我們首先關注到remainingTime,這是一個重要的變數,規定了我們dump所有程序的最長時間,因為dump程序所有執行緒的堆疊,本身就是一個重操作,何況是要dump許多程序,所以規定了發生ANR之後,dump全部程序的總時間不能超過20秒,如果超過了,馬上返回,確保ANR彈窗可以及時的彈出(或者被kill掉)。我們繼續跟到dumpJavaTracesTombstoned

private static long dumpJavaTracesTombstoned(int pid, String fileName, long timeoutMs) {

再一路追到native層負責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) {

來了來了!之前說的互動邊界終於找到了!這裡會通過sigqueue向需要dump堆疊的程序傳送SIGQUIT訊號,也就是signal 3訊號,而發生ANR的程序是一定會被dump的,也是第一個被dump的。這就意味著,只要我們能監控到系統傳送的SIGQUIT訊號,也許就能夠監控到發生了ANR。

每一個應用程序都會有一個SignalCatcher執行緒,專門處理SIGQUIT,來到art/runtime/signal_catcher.cc

void* SignalCatcher::Run(void* arg) {

WaitForSignal方法呼叫了sigwait方法,這是一個阻塞方法。這裡的死迴圈,就會一直不斷的等待監聽SIGQUIT和SIGUSR1這兩個訊號的到來。

整理一下ANR的過程:當應用發生ANR之後,系統會收集許多程序,來dump堆疊,從而生成ANR Trace檔案,收集的第一個,也是一定會被收集到的程序,就是發生ANR的程序,接著系統開始向這些應用程序傳送SIGQUIT訊號,應用程序收到SIGQUIT後開始dump堆疊。來簡單畫個示意圖:

所以,事實上程序發生ANR的整個流程,也只有dump堆疊的行為會在發生ANR的程序中執行。這個過程從收到SIGQUIT開始(圈1),到使用socket寫Trace(圈2)結束,然後再繼續回到server程序完成剩餘的ANR流程。我們就在這兩個邊界上做做文章。

首先我們肯定會想到,我們能否監聽到syste_server傳送給我們的SIGQUIT訊號呢?如果可以,我們就成功了一半。

二、監控SIGQUIT訊號

Linux系統提供了兩種監聽訊號的方法,一種是SignalCatcher執行緒使用的sigwait方法進行同步、阻塞地監聽,另一種是使用sigaction方法註冊signal handler進行非同步監聽,我們都來試試。

2.1. sigwait

我們首先嚐試前一種方法,模仿SignalCatcher執行緒,做一模一樣的事情,通過一個死迴圈sigwait,一直監聽SIGQUIT:

static void *mySigQuitCatcher(void* args) {

這個時候就有了兩個不同的執行緒sigwait同一個SIGQUIT,具體會走到哪個呢,我們在sigwait的文件中找到了這樣的描述(sigwait方法是由sigwaitinfo方法實現的):

原來當有兩個執行緒通過sigwait方法監聽同一個訊號時,具體是哪一個執行緒收到訊號時不能確定的**。不確定可不行,當然不滿足我們的需求。

3.2. Signal Handler

那我們再試下另一種方法是否可行,我們通過可以sigaction方法,建立一個Signal Handler:

void signalHandler(int sig, siginfo_t* info, void* uc) {

建立了Signal Handler之後,我們發現在同時有sigwait和signal handler的情況下,訊號沒有走到我們的signal handler而是依然被系統的Signal Catcher執行緒捕獲到了,這是什麼原因呢?

原來是Android預設把SIGQUIT設定成了BLOCKED,所以只會響應sigwait而不會進入到我們設定的handler方法中。我們通過pthread_sigmask或者sigprocmask把SIGQUIT設定為UNBLOCK,那麼再次收到SIGQUIT時,就一定會進入到我們的handler方法中。需要這樣設定:

sigset_t sigSet;

最後需要注意,我們通過Signal Handler搶到了SIGQUIT後,原本的Signal Catcher執行緒中的sigwait就不再能收到SIGQUIT了,原本的dump堆疊的邏輯就無法完成了,我們為了ANR的整個邏輯和流程跟原來完全一致,需要在Signal Handler裡面重新向Signal Catcher執行緒傳送一個SIGQUIT:

int tid = getSignalCatcherThreadId(); //遍歷/proc/[pid]目錄,找到SignalCatcher執行緒的tid

(如果缺少了重新向SignalCatcher傳送SIGQUIT的步驟,AMS就一直等不到ANR程序寫堆疊,直到20秒超時後,才會被迫中斷,而繼續之後的流程。直接的表現就是ANR彈窗非常慢(20秒超時時間),並且/data/anr目錄下無法正常生成完整的 ANR Trace檔案。)

以上就得到了一個不改變系統行為的前提下,比較完善的監控SIGQUIT訊號的機制,這也是我們監控ANR的基礎。

三、完善的ANR監控方案

監控到SIGQUIT訊號並不等於就監控到了ANR。

3.1. 誤報

充分非必要條件1:發生ANR的程序一定會收到SIGQUIT訊號;但是收到SIGQUIT訊號的程序並不一定發生了ANR。

考慮下面兩種情況:

  • 其他程序的ANR:上面提到過,發生ANR之後,發生ANR的程序並不是唯一需要dump堆疊的程序,系統會收集許多其他的程序進行dump,也就是說當一個應用發生ANR的時候,其他的應用也有可能收到SIGQUIT訊號。進一步,我們監控到SIGQUIT時,可能是監聽到了其他程序產生的ANR****,從而產生誤報。

  • 非ANR傳送SIGQUIT:傳送SIGQUIT訊號其實是很容易的一件事情,開發者和廠商都可以很容易的傳送一個SIGQUIT(java層呼叫android.os.Process.sendSignal方法;Native層呼叫kill或者tgkill方法),所以我們可能會收到非ANR流程傳送的SIGQUIT訊號,從而產生誤報。

怎麼解決這些誤報的問題呢,我重新回到ANR流程開始的地方:

void appNotResponding(String activityShortComponentName, ApplicationInfo aInfo,

在ANR彈窗前,會執行到makeAppNotRespondingLocked方法中,在這裡會給發生ANR程序標記一個NOT_RESPONDING的flag。而這個flag我們可以通過ActivityManager來獲取:

private static boolean checkErrorState() {

監控到SIGQUIT後,我們在20秒內(20秒是ANR dump的timeout時間)不斷輪詢自己是否有NOT_RESPONDING對flag,一旦發現有這個flag,那麼馬上就可以認定發生了一次ANR。

(你可能會想,有這麼方便的方法,監控SIGQUIT訊號不是多餘的嗎?直接一個死迴圈,不斷輪訓這個flag不就完事了?是的,理論上確實能這麼做,但是這麼做過於的低效、耗電和不環保外,更關鍵的是,下面漏報的問題依然無法解決)

另外,Signal Handler回撥的第二個引數siginfo_t,也包含了一些有用的資訊,該結構體的第三個欄位si_code表示該訊號被髮送的方法,SI_USER表示訊號是通過kill傳送的,SI_QUEUE表示訊號是通過sigqueue傳送的。但在Android的ANR流程中,高版本使用的是sigqueue傳送的訊號,某些低版本使用的是kill傳送的訊號,並不統一。

而第五個欄位(極少數機型上是第四個欄位)si_pid表示的是傳送該訊號的程序的pid,這裡適用幾乎所有Android版本和機型的一個條件是:如果傳送訊號的程序是自己的程序,那麼一定不是一個ANR。可以通過這個條件排除自己傳送SIGQUIT,而導致誤報的情況。

3.2. 漏報

充分非必要條件2:程序處於NOT_RESPONDING的狀態可以確認該程序發生了ANR。但是發生ANR的程序並不一定會被設定為NOT_RESPONDING狀態。

考慮下面兩種情況:

  • 後臺ANR(SilentAnr):之前分析ANR流程我們可以知道,如果ANR被標記為了後臺ANR(即SilentAnr),那麼殺死程序後就會直接return,並不會走到產生程序錯誤狀態的邏輯。這就意味著,後臺ANR沒辦法捕捉到,而後臺ANR的量同樣非常大,並且後臺ANR會直接殺死程序,對使用者的體驗也是非常負面的,這麼大一部分ANR監控不到,當然是無法接受的。

  • 閃退ANR:除此之外,我們還發現相當一部分機型(例如OPPO、VIVO兩家的高Android版本的機型)修改了ANR的流程,即使是發生在前臺的ANR,也並不會彈窗,而是直接殺死程序,即閃退。這部分的機型覆蓋的使用者量也非常大。並且,確定兩家今後的新裝置會一直維持這個機制。

所以我們需要一種方法,在收到SIGQUIT訊號後,能夠非常快速的偵查出自己是不是已處於ANR的狀態,進行快速的dump和上報。很容易想到,我們可以通過主執行緒是否處於卡頓狀態來判斷。那麼怎麼最快速的知道主執行緒是不是卡住了呢?上一篇文章中,分析Sync Barrier洩漏問題時,我們反射過主執行緒Looper的mMessage物件,該物件的when變數,表示的就是當前正在處理的訊息入隊的時間,我們可以通過when變數減去當前時間,得到的就是等待時間,如果等待時間過長,就說明主執行緒是處於卡住的狀態,這時候收到SIGQUIT訊號基本上就可以認為的確發生了一次ANR:

private static boolean isMainThreadStuck(){

我們通過上面幾種機制來綜合判斷收到SIGQUIT訊號後,是否真的發生了一次ANR,最大程度地減少誤報和漏報,才是一個比較完善的監控方案。

3.3. 額外收穫:獲取ANR Trace

回到之前畫的ANR流程示意圖,Signal Catcher執行緒寫Trace(圈2)也是一個邊界,並且是通過socket的write方法來寫Trace的,如果我們能夠hook到這裡的write,我們甚至就可以拿到系統dump的ANR Trace內容。這個內容非常全面,包括了所有執行緒的各種狀態、鎖和堆疊(包括native堆疊),對於我們排查問題十分有用,尤其是一些native問題和死鎖等問題。Native Hook我們採用PLT Hook 方案,這種方案在微信上已經被驗證了其穩定性是可控的。

int (*original_connect)(int __fd, const struct sockaddr* __addr, socklen_t __addr_length);

其中有幾點需要注意:

  • 只Hook ANR流程:有些情況下,基礎庫中的connect/open/write方法可能呼叫的比較頻繁,我們需要把hook的影響降到最低。所以我們只會在接收到SIGQUIT訊號後(重新發送SIGQUIT訊號給Signal Catcher前)進行hook,ANR流程結束後再unhook。

  • 只處理Signal Catcher執行緒open/connect後的第一次write:除了Signal Catcher執行緒中的dump trace的流程,其他地方呼叫的write方法我們並不關心,並不需要處理。例如,dump trace的流程會在在write方法前,系統會先使用connet方法連結一個path為“/dev/socket/tombstoned_java_trace”的socket,我們可以hook connect方法,拿到這個socket的name,我們只處理connect這個socket後,相同執行緒(即Signal Catcher執行緒)的第一次write,這次write的內容才是我們唯一關心的。

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

這個Hook Trace的方案,不僅僅可以用來查ANR問題,任何時候我們都可以手動向自己傳送一個SIGQUIT訊號,從而hook到當時的Trace。Trace的內容對於我們排查執行緒死鎖,執行緒異常,耗電等問題都非常有幫助。

這樣我們就得到了一個完善的ANR監控方案,這套方案在微信上平穩運行了很長一段時間,給我們評估和優化微信Android客戶端的質量提供了非常重要根據和方向。

關注我,每天分享知識乾貨