今日頭條 ANR 優化實踐系列 - 設計原理及影響因素
寫在前面
ANR 問題,對於從事 Android 開發的同學來說並不陌生,日常開發中,經常會遇到應用乃至系統層面引起的各種問題,很多時候因爲不瞭解其運行原理,在面對該類問題時可能會一頭霧水。與此同時,因爲現有監控能力不足或獲取信息有限,使得這類問題如同鏡中花水中月,讓我們在追求真理的道路上舉步維艱。如下圖:
工作中在幫助大家分析問題時,發現有不少同學問到,在哪裏可以更加系統的學習?於是本人抱着 “授人以魚,不如授人以漁” 的態度,結合個人理解和工作實踐,接下來將從設計原理、影響要素、工具建設、分析思路,案例實戰、優化探索等幾個篇章,對 ANR 方向進行一次全面的總結,希望幫助大家在今後的工作中更好地理解和應對以下問題:
-
什麼是 ANR?
-
系統是如何設計 ANR 的?
-
發生 ANR 時系統都會獲取哪些信息以及工作流程?
-
導致 ANR 的原因有哪些?
-
遇到這類問題該如何分析?
-
如何能更加快速準確的定位問題?
-
面對這類問題我們能主動做些什麼?
簡述
在正式分析 ANR 問題之前,先來看看下面這些問題:
-
系統是如何設計 ANR 的,都有哪些服務或者組件會發生 ANR?
-
發生 ANR 的時候,系統又是如何工作的,都會獲取哪些信息?
-
影響 ANR 的場景有哪些?我們是如何對其進行歸類的?
瞭解這些有助於我們在面對各種問題時,做到有的放矢,下面我們就來介紹並回答這些問題。
ANR 設計原理
ANR 全稱 Applicatipon No Response;Android 設計 ANR 的用意,是系統通過與之交互的組件 (Activity,Service,Receiver,Provider) 以及用戶交互 (InputEvent) 進行超時監控,以判斷應用進程 (主線程) 是否存在卡死或響應過慢的問題,通俗來說就是很多系統中看門狗 (watchdog) 的設計思想。
組件超時分類
系統在通過 Binder 通信嚮應用進程發送上述組件消息或 Input 事件時,在 AMS 或 Input 服務端同時設置一個異步超時監控。當然針對不同類型事件,設置的超時時長也存在差別,以下是 Android 系統對不同類型的超時閾值設置:
(圖片僅供參考,國內廠商可能會有調整,每個廠商的標準也存在差異)
Broadcast 超時原理舉例
在瞭解不同類型消息的超時閾值之後,我們再來了解一下超時監控的設計原理。
以 BroadCastReceiver 廣播接收超時爲例,廣播分爲有序廣播和無序廣播,同時又有前臺廣播和後臺廣播之分;只針對有序廣播設置超時監控機制,並根據前臺廣播和後臺廣播的廣播類型決定了超時時長;例如後臺廣播超時時長 60S,前臺廣播超時時長只有 10S; 下面我們結合代碼實現來看一下廣播消息的發送過程。
- 無序廣播:
對於無序廣播,系統在蒐集所有接收者之後一次性全部發送完畢,如下圖;
通過上圖我們看到無序廣播是沒有設置超時監聽機制的,一次性發送給所有接收者,對於應用側何時接收和響應完全不關心 (相當於 UDP 傳輸)。
- 有序廣播:
再來看一下有序廣播的發送和接收邏輯,同樣在系統 AMS 服務中,BoradCastQueue 獲取當前正在發送的廣播消息,並取出下一個廣播接收者,更新發送時間戳,以此時間計算並設置超時時間 (但是系統在此進行了一些優化處理,以避免每次廣播正常接收後,都需要取消超時監控然後又重新設置,而是採用一種對齊的方式進行復用)。最後將該廣播發送給接收者,接收到客戶端的完成通知之後,再發送下一個,整個過程如此反覆。
在客戶端進程中,Binder 線程接收到 AMS 服務發送過來的廣播消息之後,會將此消息進行封裝成一個 Message,然後將 Message 發送到主線程消息隊列 (插入到消息隊列當前時間節點的位置,也正是基於此類設計導致較多消息調度及時性的問題,後面我們將詳細介紹),消息接收邏輯如下:
正常情況下,很多廣播請求都會在客戶端及時響應,然後通知到系統 AMS 服務取消本次超時監控。但是在部分業務場景或系統場景異常的情況下,發送的廣播未及時調度,沒有及時通知到系統服務,便會在系統服務側觸發超時,判定應用進程響應超時。AMS 響應超時代碼邏輯如下:
1final void broadcastTimeoutLocked(boolean fromMsg) {
2 ......
3 long now = SystemClock.uptimeMillis();
4 BroadcastRecord r = mOrderedBroadcasts.get(0);
5 if (fromMsg) {
6 //我們剛纔提到的時間對齊方式,避免頻繁取消和設置消息超時
7 long timeoutTime = r.receiverTime + mTimeoutPeriod;
8 if (timeoutTime > now) {
9 setBroadcastTimeoutLocked(timeoutTime);
10 return;
11 }
12 }
13 ......
14 ......
15 Object curReceiver;
16 if (r.nextReceiver > 0) {
17 //獲取當前超時廣播接收者
18 curReceiver = r.receivers.get(r.nextReceiver-1);
19 r.delivery[r.nextReceiver-1] = BroadcastRecord.DELIVERY_TIMEOUT;
20 } else {
21 curReceiver = r.curReceiver;
22 }
23 Slog.w(TAG, "Receiver during timeout of " + r + " : " + curReceiver);
24 ......
25 ......
26 if (app != null) {
27 anrMessage = "Broadcast of " + r.intent.toString();
28 }
29 ......
30 if (!debugging && anrMessage != null) {
31 //開始通知AMS服務處理當前超時行爲
32 mHandler.post(new AppNotResponding(app, anrMessage));
33 }
34 }
35
36
到這裏,廣播發送和超時監控邏輯的分析就基本結束了,通過介紹,我們基本知道了廣播超時機制是如何設計和工作的,整體流程圖示意圖如下:
ANR Trace Dump 流程
上面我們以廣播接收爲例,介紹了系統監控原理,下面再來介紹一下,發生 ANR 時系統工作流程。
ANR 信息獲取:
繼續以廣播接收爲例,在上面介紹到當判定超時後,會調用系統服務 AMS 接口,蒐集本次 ANR 相關信息並存檔 (data/anr/trace,data/system/dropbox),入口如下。
進入系統服務 AMS 之後,AppError 先進行場景判斷,以過濾當前進程是不是已經發生並正在執行 Dump 流程,或者已經發生 Crash,或者已經被系統 Kill 之類的情況。並且還考慮了系統是否正在關機等場景,如果都不符合上述條件,則認爲當前進程真的發生 ANR。
接下來系統再判斷當前 ANR 進程對用戶是否可感知,如後臺低優先級進程 (沒有重要服務或者 Activity 界面)。
然後開始統計與該進程有關聯的進程,或系統核心服務進程的信息;例如與應用進程經常交互的 SurfaceFligner,SystemServer 等系統進程,如果這些系統服務進程在響應時被阻塞,那麼將導致應用進程 IPC 通信過程被卡死。
首先把自身進程 (系統服務 SystemServer) 加進來,邏輯如下:
接着獲取其它系統核心進程,因爲這些服務進程是 Init 進程直接創建的,並不在 SystemServer 或 Zygote 進程管理範圍。
在蒐集完第一步信息之後,接下來便開始統計各進程本地的更多信息,如虛擬機相關信息、Java 線程狀態及堆棧。以便於知道此刻這些進程乃至系統都發生了什麼情況。理想很豐滿,現實很骨感,後面我們會重點講述爲何有此感受。
系統爲何要收集其它進程信息呢?因爲從性能角度來說,任何進程出現高 CPU 或高 IO 情況,都會搶佔系統資源,進而影響其它進程調度不及時的現象。下面從代碼角度看看系統 dump 流程:
1private static void dumpStackTraces(String tracesFile, ArrayList<Integer> firstPids, ArrayList<Integer> nativePids, ArrayList<Integer> extraPids,
2 boolean useTombstonedForJavaTraces) {
3 ......
4 ......
5 //考慮到性能影響,一次dump最多持續20S,否則放棄後續進程直接結束
6 remainingTime = 20 * 1000;
7 try {
8 ......
9 //按照優先級依次獲取各個進程trace日誌
10 int num = firstPids.size();
11 for (int i = 0; i < num; i++) {
12 final long timeTaken;
13 if (useTombstonedForJavaTraces) {
14 timeTaken = dumpJavaTracesTombstoned(firstPids.get(i), tracesFile, remainingTime);
15 } else {
16 timeTaken = observer.dumpWithTimeout(firstPids.get(i), remainingTime);
17 }
18
19 remainingTime -= timeTaken;
20 if (remainingTime <= 0) {
21 //已經超時,則不再進行後續進程的dump操作
22 return;
23 }
24 }
25 }
26 }
27 //按照優先級依次獲取各個進程trace日誌
28 for (int pid : nativePids) {
29 final long nativeDumpTimeoutMs = Math.min(NATIVE_DUMP_TIMEOUT_MS, remainingTime);
30
31 final long start = SystemClock.elapsedRealtime();
32 Debug.dumpNativeBacktraceToFileTimeout(
33 pid, tracesFile, (int) (nativeDumpTimeoutMs / 1000));
34 final long timeTaken = SystemClock.elapsedRealtime() - start;
35
36 remainingTime -= timeTaken;
37 if (remainingTime <= 0) {
38 //已經超時,則不再進行後續進程的dump操作
39 return;
40 }
41 }
42 }
43 //按照優先級依次獲取各個進程trace日誌
44 for (int pid : extraPids) {
45 final long timeTaken;
46 if (useTombstonedForJavaTraces) {
47 timeTaken = dumpJavaTracesTombstoned(pid, tracesFile, remainingTime);
48 } else {
49 timeTaken = observer.dumpWithTimeout(pid, remainingTime);
50 }
51
52 remainingTime -= timeTaken;
53 if (remainingTime <= 0) {
54 //已經超時,則不再進行後續進程的dump操作
55 return;
56 }
57 }
58 }
59 }
60 ......
61 }
62
63
Dump Trace 流程
出於安全考慮,進程之間是相互隔離的,即使是系統進程也無法直接獲取其它進程相關信息。因此需要藉助 IPC 通信的方式,將指令發送到目標進程,目標進程接收到信號後,協助完成自身進程 Dump 信息併發送給系統進程。以 AndroidP 系統爲例,大致流程圖如下:
關於應用進程接收信號和響應能力,是在虛擬機內部實現的,在虛擬機啓動過程中進行信號註冊和監聽 (SIGQUIT),初始化邏輯如下:
SignalCatcher 線程接收到信號後,首先 Dump 當前虛擬機有關信息,如內存狀態,對象,加載 Class,GC 等等,接下來設置各線程標記位 (check_point),以請求線程起態 (suspend)。其它線程運行過程進行上下文切換時,會檢查該標記,如果發現有掛起請求,會主動將自己掛起。等到所有線程掛起後,SignalCatcher 線程開始遍歷 Dump 各線程的堆棧和線程數據,結束之後再喚醒線程。期間如果某些線程一直無法掛起直到超時,那麼本次 Dump 流程則失敗,並主動拋出超時異常。
根據上面梳理的流程,SignalCatcher 獲取各線程信息的工作過程,示意圖如下:
到這裏,基本介紹完了系統設計原理,並以廣播發送爲例說明系統是如何判定 ANR 的,以及發生 ANR 後,系統是如何獲取系統信息和進程信息,以及其他進程是如何協助系統進程完成日誌收集的。
整體來看鏈路比較長,而且涉及到與很多進程交互,同時爲了進一步降低對應用乃至系統的影響,系統在很多環節都設置大量超時檢測。而且從上面流程可以看到發生 ANR 時,系統進程除了發送信號給其它進程之外,自身也 Dump Trace,並獲取系統整體及各進程 CPU 使用情況,且將其它進程 Dump 發送的數據寫到文件中。因此這些開銷將會導致系統進程在 ANR 過程承擔很大的負載,這是爲什麼我們經常在 ANR Trace 中看到 SystemServer 進程 CPU 佔比普遍較高的主要原因。
應用層如何判定 ANR
Android M(6.0) 版本之後,應用側無法直接通過監聽 data/anr/trace 文件,監控是否發生 ANR,那麼大家又有什麼其它手段去判定 ANR 呢?下面我們簡單介紹一下
站在應用側角度來看,因爲系統沒有提供太友好的機制,去主動通知應用是否發生 ANR,而且很多信息更是對應用屏蔽了訪問權限,但是對於三方 App 來說,也需要關注基本的用戶體驗,因此很多公司也進行了大量的探索,並給出了不同的解決思路,目前瞭解到的方案 (思路) 主要有下面 2 種:
- 主線程 watchdog 機制
核心思想是在應用層定期向主線程設置探測消息,並在異步設置超時監測,如在規定的時間內沒有收到發送的探測消息狀態更新,則判定可能發生 ANR,爲什麼是可能發生 ANR?因爲還需要進一步從系統服務獲取相關數據 (下面會講到如何獲取),進一步判定是否真的發生 ANR。
- 監聽 SIGNALQUIT 信號
該方案在很多公司有應用,網上也有相關介紹,這裏主要介紹一下思路。我們在上面提到了虛擬機是通過註冊和監聽 SIGNALQUIT 信號的方式執行請求的,而對於信號機制有了解的同學馬上就可以猜到,我們也可以在應用層參考此方式註冊相同信號去監聽。不過要注意的是註冊之後虛擬機之前註冊的就會被覆蓋,需要在適當的時候進行恢復,否則小心繫統 (廠商) 找上門。
當接收到該信號時,過濾場景,確定是發生用戶可感知的 ANR 之後,從 Java 層獲取各線程堆棧,或通過反射方式獲取到虛擬機內部 Dump 線程堆棧的接口,在內存映射的函數地址,強制調用該接口,並將數據重定向輸出到本地。
該方案從思路上來說優於第一種方案,並且遵循系統信息獲取方式,獲取的線程信息及虛擬機信息更加全面,但缺點是對性能影響比較大,對於複雜的 App 來說,統計其耗時,部分場景一次 Dump 耗時可能要超過 10S。
應用層如何獲取 ANR Info
上面提到無論是 Watchdog 還是監聽信號的方式,都需要結論進一步過濾,以確保收集我們想要的 ANR 場景,因此需要利用系統提供的接口,進一步判定當前應用是否發生問題 (ANR,Crash);
與此同時,除了需要獲取進程中各線程狀態之外,我們也需要知道系統乃至其他進程的一些狀態,如系統 CPU,Mem,IO 負載,關鍵進程的 CPU 使用率等等,便於推測發生問題時系統環境是否正常;
獲取信息相關接口類如下:
通過該接口獲取的相關信息,示意如下,其中下圖紅框選中的關鍵字,我們在後續 ANR 分析思路一章,會對其進行詳細釋義:
影響因素
上面主要介紹系統針對各種類型的消息是如何設置超時監控,以及監測到超時之後,系統側和應用側如何獲取各類信息的工作流程。在對這些有所瞭解之後,接下來再看看 ANR 問題是如何產生的,以及我們對影響 ANR 因素的類型劃分。
舉個例子:
在工作中,有同學問到 “我的 Service” 邏輯很簡單,爲何會 ANR 呢?其實通過堆棧和監控工具可以發現,他所說的業務 Service,其實都還沒來得及被調度。原來該同學是從我們的內部監控平臺上看到是該 Service 發生導致的 ANR,如下圖:
下面我們就來回答一下爲何會出現上面的這類現象?
問題答疑
通過前面的講解,我們可以發現,系統服務 (AMS,InputService) 在將具有超時屬性的消息,如創建 Service,Receiver,Input 事件等,通過 Binder 或者其它 IPC 的方式發送到目標進程之後,便啓動異步超時監測。而這種性質的監測完全是一種黑盒監測,並不是真的監控發送的消息在真實執行過程中是否超時,也就是說系統不管發送的這個消息有沒有被執行,或者真實執行過程耗時有多久,只要在監控超時到來之前,服務端沒有接收到通知,那麼就判定爲超時。
同時在前面我們講到,當系統側將消息發送給目標進程之後,其客戶端進程的 Binder 線程接收到該消息後,會按時間順序插入到消息隊列;在後續等待執行過程中,會有下面幾種情況發生:
-
啓動進程啓動場景,大量業務或基礎庫需要初始化,在消息入隊之前,已經有很多消息待調度;
-
有些場景有可能只是少量消息,但是其中有一個或多個消息耗時很長;
-
有些場景其他進程或者系統負載特別高,整個系統都變得有些卡頓。
上述這些場景都會導致發送的消息還沒來得及執行,就可能已經被系統判定成爲超時問題,然而此時進程接收信號後,主線程 Dump 的是當前某個消息執行過程的業務堆棧 (邏輯)。
所以總結來說,發生 ANR 問題時,Trace 堆棧很多情況下都不是 RootCase。而系統 ANR Info 中提示某個 Service 或 Receiver 導致的 ANR 在很大程度上,並不是這些組件自身問題。
那麼影響 ANR 的場景具體可以分爲哪幾類呢,下面我們就來聊一聊。
影響因素分類
結合我們在系統側和應用側的工作經歷,以及對該類問題的理解,我們將可能導致 ANR 的影響要素分爲下面幾個方面,影響環境分爲應用內部環境和系統環境;即 系統負載正常,但是應用內部主線程消息過多或耗時嚴重;另外一類則是系統或應用內部其它線程或資源負載過高,主線程調度被嚴重搶佔;系統負載正常,主線程調度問題,總體來說包括以下幾種:
-
當前 Trace 堆棧所在業務耗時嚴重;
-
當前 Trace 堆棧所在業務耗時並不嚴重,但是歷史調度有一個嚴重耗時;
-
當前 Trace 堆棧所在業務耗時並不嚴重,但是歷史調度有多個消息耗時;
-
當前 Trace 堆棧所在業務耗時並不嚴重,但是歷史調度存在巨量重複消息 (業務頻繁發送消息);
-
當前 Trace 堆棧業務邏輯並不耗時,但是其他線程存在嚴重資源搶佔,如 IO,Mem,CPU;
-
當前 Trace 堆棧業務邏輯並不耗時,但是其他進程存在嚴重資源搶佔,如 IO,Mem,CPU;
下面我們就來分別介紹一下這幾種場景以及表現情況:
當前主線程正在調度的消息耗時嚴重
理論上某個消息耗時越嚴重,那麼這個消息造成的卡頓或者 ANR 的概率就越大,這種場景在線上經常發生,相對來說比較容易排查,也是業務開發同學分析該類問題的常規思路。
發生 ANR 時主線程消息調度示意圖如下:
如果之前某個歷史消息嚴重耗時,但是直到該消息執行完畢,系統服務仍然沒有達到觸發超時的臨界點,後續主線程繼續調度其它消息時,系統判定響應超時,那麼正在執行的業務場景很不幸被命中,而當前正在執行的業務邏輯可能很簡單。
這種場景在線上大量存在,因爲比較隱蔽,所以會給很多同學帶來困惑,後面會在 ANR 實例分析中對其進行重點介紹。發生 ANR 時主線程消息調度示意圖如下:
除了上述兩種場景,還有一種情況就是存在多個消息耗時嚴重的情況,直到後面主線程調度其它消息時,系統判定響應超時,那麼正在執行的業務場景很不幸被命中;這種場景在實際環境中也是普遍存在的,這類問題更加隱蔽,並且在分析和問題歸因上,也很難清晰的劃清界限,問題治理上需要推動多個業務場景進行優化。(後面會在 ANR 實例分析中對其進行重點介紹)
發生 ANR 時主線程消息調度示意圖如下:
上面我們講到的是有一個或多個消息耗時較長,還有另外一種情況就是業務邏輯發生異常或者業務線程與主線程頻繁交互,大量消息堆積在消息隊列,這時對於後續追加到消息隊列的消息來說,儘管不存在單個耗時嚴重的消息,但是消息太密集導致一段時間內同樣很難被及時調度,因此這種場景也會造成消息調度不及時,進而導致響應超時問題。(後面會在 ANR 實例分析中對其進行介紹)
發生 ANR 時主線程消息調度示意圖如下:
應用進程或系統 (包括其它進程) 負載過重
除了上面列舉了一些主線程消息耗時嚴重或者消息過多,導致的消息調度不及時的可能引起的問題之外,還有一種我們在線上經常遇到的場景,那就是進程或系統本身負載很重,如高 CPU,高 IO,低內存 (應用內內存抖動頻繁 GC,系統內存回收) 等等。這種情況出現之後,也很導致應用或整體系統性能變差,最終導致一系列超時問題。
針對這種情況,具體到主線程消息調度上表現來看,就是很多消息耗時都比較嚴重,而且每次消息調度統計的 Wall Duration(絕對時間:包括正常調度和等待,休眠時間) 和 CPU Duration(絕對時間:只包括 CPU 執行時間) 相差很大,如果出現這種情況我們則認爲系統負載可能發生了異常,需要藉助系統信息進一步對比分析。這種情況不僅影響當前應用,也會影響其他應用乃至系統自身。
發生 ANR 時主線程消息調度示意圖如下:
總結
通過上面的介紹,我們介紹了 ANR 的設計原理及工作過程,對影響 ANR 的因素和分類也有了進一步認識。從歸類上我們可以發現,影響 ANR 的場景會有很多種,甚至很多時候都是層層疊加導致,所以可以借用一句話來形容:「當 ANR 發生時,沒有一個消息是無辜的」。
後續
依靠系統現有的監控能力,並不能直觀的體現上面列舉的衆多場景,更無法直觀告訴我們 ANR 發生前主線程調度情況。僅僅依靠 ANR 時獲取系統及 Top 進程的相關信息和一些 Log 日誌,很多數時候只能幫我們完成第一階段的定位,如系統負載過重,主線程過於繁忙等結論。卻無法更進一步深入分析和解決問題,尤其是一些線下難以復現的問題。
對於我們每個人來說,工作的目標不僅僅是定位方向,更重要的是解決問題。那麼怎麼才能更好的解決上述系統監控能力不完善以及應用側信息盲區的問題呢?這就是我們下一期要重點介紹的 “監控工具”,一個優秀的工具,不僅可以幫助我們在解決常規問題時達到一錘定音的效果,在面對更加複雜隱蔽的問題時,也能爲我們打開視野,提供更多方向,下週的文章我們就去看看它是如何設計及運用的。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/ApNSEWxQdM19QoCNijagtg