今日頭條 ANR 優化實踐系列 - 設計原理及影響因素

寫在前面

ANR 問題,對於從事 Android 開發的同窗來講並不陌生,平常開發中,常常會遇到應用乃至系統層面引發的各類問題,不少時候由於不瞭解其運行原理,在面對該類問題時可能會一頭霧水。與此同時,由於現有監控能力不足或獲取信息有限,使得這類問題如同鏡中花水中月,讓咱們在追求真理的道路上舉步維艱。以下圖:安全

工做中在幫助你們分析問題時,發現有很多同窗問到,在哪裏能夠更加系統的學習?因而本人抱着「授人以魚,不如授人以漁」的態度,結合我的理解和工做實踐,接下來將從設計原理、影響要素、工具建設、分析思路,案例實戰、優化探索等幾個篇章,對 ANR 方向進行一次全面的總結,但願幫助你們在從此的工做中更好地理解和應對如下問題:markdown

  1. 什麼是 ANR?
  2. 系統是如何設計 ANR 的?
  3. 發生 ANR 時系統都會獲取哪些信息以及工做流程?
  4. 致使 ANR 的緣由有哪些?
  5. 遇到這類問題該如何分析?
  6. 如何能更加快速準確的定位問題?
  7. 面對這類問題咱們能主動作些什麼?

簡述

在正式分析 ANR 問題以前,先來看看下面這些問題:架構

  • 系統是如何設計 ANR 的,都有哪些服務或者組件會發生 ANR?
  • 發生 ANR 的時候,系統又是如何工做的,都會獲取哪些信息?
  • 影響 ANR 的場景有哪些?咱們是如何對其進行歸類的?

瞭解這些有助於咱們在面對各類問題時,作到有的放矢,下面咱們就來介紹並回答這些問題。併發

ANR 設計原理

ANR 全稱 Applicatipon No Response;Android 設計 ANR 的用意,是系統經過與之交互的組件(Activity,Service,Receiver,Provider)以及用戶交互(InputEvent)進行超時監控,以判斷應用進程(主線程)是否存在卡死或響應過慢的問題,通俗來講就是不少系統中看門狗(watchdog)的設計思想。app

組件超時分類

系統在經過 Binder 通訊嚮應用進程發送上述組件消息或 Input 事件時,在 AMS 或 Input 服務端同時設置一個異步超時監控。固然針對不一樣類型事件,設置的超時時長也存在差異,如下是 Android 系統對不一樣類型的超時閾值設置:異步

(圖片僅供參考,國內廠商可能會有調整,每一個廠商的標準也存在差別)ide

Broadcast 超時原理舉例

在瞭解不一樣類型消息的超時閾值以後,咱們再來了解一下超時監控的設計原理。函數

以 BroadCastReceiver 廣播接收超時爲例,廣播分爲有序廣播和無序廣播,同時又有前臺廣播和後臺廣播之分;只針對有序廣播設置超時監控機制,並根據前臺廣播和後臺廣播的廣播類型決定了超時時長;例如後臺廣播超時時長 60S,前臺廣播超時時長只有 10S; 下面咱們結合代碼實現來看一下廣播消息的發送過程。工具

  • 無序廣播:

對於無序廣播,系統在蒐集全部接收者以後一次性所有發送完畢,以下圖:oop

經過上圖咱們看到無序廣播是沒有設置超時監聽機制的,一次性發送給全部接收者,對於應用側什麼時候接收和響應徹底不關心(至關於 UDP 傳輸)。

  • 有序廣播:

再來看一下有序廣播的發送和接收邏輯,一樣在系統 AMS 服務中,BoradCastQueue 獲取當前正在發送的廣播消息,並取出下一個廣播接收者,更新發送時間戳,以此時間計算並設置超時時間(可是系統在此進行了一些優化處理,以免每次廣播正常接收後,都須要取消超時監控而後又從新設置,而是採用一種對齊的方式進行復用)。最後將該廣播發送給接收者,接收到客戶端的完成通知以後,再發送下一個,整個過程如此反覆。

在客戶端進程中,Binder 線程接收到 AMS 服務發送過來的廣播消息以後,會將此消息進行封裝成一個 Message,而後將 Message 發送到主線程消息隊列(插入到消息隊列當前時間節點的位置,也正是基於此類設計致使較多消息調度及時性的問題,後面咱們將詳細介紹),消息接收邏輯以下:

正常狀況下,不少廣播請求都會在客戶端及時響應,而後通知到系統 AMS 服務取消本次超時監控。可是在部分業務場景或系統場景異常的狀況下,發送的廣播未及時調度,沒有及時通知到系統服務,便會在系統服務側觸發超時,斷定應用進程響應超時。AMS 響應超時代碼邏輯以下:

final void broadcastTimeoutLocked(boolean fromMsg) {
        ......
        long now = SystemClock.uptimeMillis();
        BroadcastRecord r = mOrderedBroadcasts.get(0);
        if (fromMsg) {
            //咱們剛纔提到的時間對齊方式,避免頻繁取消和設置消息超時
            long timeoutTime = r.receiverTime + mTimeoutPeriod;
            if (timeoutTime > now) {
                setBroadcastTimeoutLocked(timeoutTime);
                return;
            }
        }
        ......
        ......
        Object curReceiver;
        if (r.nextReceiver > 0) {
            //獲取當前超時廣播接收者
            curReceiver = r.receivers.get(r.nextReceiver-1);
            r.delivery[r.nextReceiver-1] = BroadcastRecord.DELIVERY_TIMEOUT;
        } else {
            curReceiver = r.curReceiver;
        }
        Slog.w(TAG, "Receiver during timeout of " + r + " : " + curReceiver);
        ......
        ......
        if (app != null) {
            anrMessage = "Broadcast of " + r.intent.toString();
        }
        ......
        if (!debugging && anrMessage != null) {
             //開始通知AMS服務處理當前超時行爲
            mHandler.post(new AppNotResponding(app, anrMessage));
        }
    }

複製代碼

到這裏,廣播發送和超時監控邏輯的分析就基本結束了,經過介紹,咱們基本知道了廣播超時機制是如何設計和工做的,總體流程圖示意圖以下:

ANR Trace Dump 流程

上面咱們以廣播接收爲例,介紹了系統監控原理,下面再來介紹一下,發生 ANR 時系統工做流程。

ANR 信息獲取:

繼續以廣播接收爲例,在上面介紹到當斷定超時後,會調用系統服務 AMS 接口,蒐集本次 ANR 相關信息並存檔(data/anr/trace,data/system/dropbox),入口以下:

進入系統服務 AMS 以後,AppError 先進行場景判斷,以過濾當前進程是否是已經發生並正在執行 Dump 流程,或者已經發生 Crash,或者已經被系統 Kill 之類的狀況。而且還考慮了系統是否正在關機等場景,若是都不符合上述條件,則認爲當前進程真的發生 ANR。

image.png

接下來系統再判斷當前 ANR 進程對用戶是否可感知,如後臺低優先級進程(沒有重要服務或者 Activity 界面)。

而後開始統計與該進程有關聯的進程,或系統核心服務進程的信息;例如與應用進程常常交互的 SurfaceFligner,SystemServer 等系統進程,若是這些系統服務進程在響應時被阻塞,那麼將致使應用進程 IPC 通訊過程被卡死。

首先把自身進程(系統服務 SystemServer)加進來,邏輯以下:

接着獲取其它系統核心進程,由於這些服務進程是 Init 進程直接建立的,並不在 SystemServer 或 Zygote 進程管理範圍。

10.png

11.png

在蒐集完第一步信息以後,接下來便開始統計各進程本地的更多信息,如虛擬機相關信息、Java 線程狀態及堆棧。以便於知道此刻這些進程乃至系統都發生了什麼狀況。理想很豐滿,現實很骨感,後面咱們會重點講述爲什麼有此感覺。

12.png

系統爲什麼要收集其它進程信息呢?由於從性能角度來講,任何進程出現高 CPU 或高 IO 狀況,都會搶佔系統資源,進而影響其它進程調度不及時的現象。下面從代碼角度看看系統 dump 流程:

private static void dumpStackTraces(String tracesFile, ArrayList<Integer> firstPids, ArrayList<Integer> nativePids, ArrayList<Integer> extraPids, boolean useTombstonedForJavaTraces) {
        ......
        ......
        //考慮到性能影響,一次dump最多持續20S,不然放棄後續進程直接結束
        remainingTime = 20 * 1000;
        try {
                ......
                //按照優先級依次獲取各個進程trace日誌
                int num = firstPids.size();
                for (int i = 0; i < num; i++) {
                    final long timeTaken;
                    if (useTombstonedForJavaTraces) {
                        timeTaken = dumpJavaTracesTombstoned(firstPids.get(i), tracesFile, remainingTime);
                    } else {
                        timeTaken = observer.dumpWithTimeout(firstPids.get(i), remainingTime);
                    }

                    remainingTime -= timeTaken;
                    if (remainingTime <= 0) {
                        //已經超時,則再也不進行後續進程的dump操做
                        return;
                    }
                    }
                }
            }
            //按照優先級依次獲取各個進程trace日誌
                for (int pid : nativePids) {
                    final long nativeDumpTimeoutMs = Math.min(NATIVE_DUMP_TIMEOUT_MS, remainingTime);

                    final long start = SystemClock.elapsedRealtime();
                    Debug.dumpNativeBacktraceToFileTimeout(
                            pid, tracesFile, (int) (nativeDumpTimeoutMs / 1000));
                    final long timeTaken = SystemClock.elapsedRealtime() - start;

                    remainingTime -= timeTaken;
                    if (remainingTime <= 0) {
                        //已經超時,則再也不進行後續進程的dump操做
                        return;
                    }
                }
            }
            //按照優先級依次獲取各個進程trace日誌
                for (int pid : extraPids) {
                    final long timeTaken;
                    if (useTombstonedForJavaTraces) {
                        timeTaken = dumpJavaTracesTombstoned(pid, tracesFile, remainingTime);
                    } else {
                        timeTaken = observer.dumpWithTimeout(pid, remainingTime);
                    }

                    remainingTime -= timeTaken;
                    if (remainingTime <= 0) {
                        //已經超時,則再也不進行後續進程的dump操做
                        return;
                    }
                }
            }
        }
        ......
    }
複製代碼

Dump Trace 流程

出於安全考慮,進程之間是相互隔離的,即便是系統進程也沒法直接獲取其它進程相關信息。所以須要藉助 IPC 通訊的方式,將指令發送到目標進程,目標進程接收到信號後,協助完成自身進程 Dump 信息併發送給系統進程。以 AndroidP 系統爲例,大體流程圖以下:

關於應用進程接收信號和響應能力,是在虛擬機內部實現的,在虛擬機啓動過程當中進行信號註冊和監聽(SIGQUIT),初始化邏輯以下:

SignalCatcher 線程接收到信號後,首先 Dump 當前虛擬機有關信息,如內存狀態,對象,加載 Class,GC 等等,接下來設置各線程標記位(check_point),以請求線程起態(suspend)。其它線程運行過程進行上下文切換時,會檢查該標記,若是發現有掛起請求,會主動將本身掛起。等到全部線程掛起後,SignalCatcher 線程開始遍歷 Dump 各線程的堆棧和線程數據,結束以後再喚醒線程。期間若是某些線程一直沒法掛起直到超時,那麼本次 Dump 流程則失敗,並主動拋出超時異常。

根據上面梳理的流程,SignalCatcher 獲取各線程信息的工做過程,示意圖以下:

17.png

到這裏,基本介紹完了系統設計原理,並以廣播發送爲例說明系統是如何斷定 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 種:

  1. 主線程 watchdog 機制

核心思想是在應用層按期向主線程設置探測消息,並在異步設置超時監測,如在規定的時間內沒有收到發送的探測消息狀態更新,則斷定可能發生 ANR,爲何是可能發生 ANR?由於還須要進一步從系統服務獲取相關數據(下面會講到如何獲取),進一步斷定是否真的發生 ANR。

  1. 監聽 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 時主線程消息調度示意圖以下:

image.png

相同消息高頻執行(業務邏輯異常)

上面咱們講到的是有一個或多個消息耗時較長,還有另一種狀況就是業務邏輯發生異常或者業務線程與主線程頻繁交互,大量消息堆積在消息隊列,這時對於後續追加到消息隊列的消息來講,儘管不存在單個耗時嚴重的消息,可是消息太密集致使一段時間內一樣很難被及時調度,所以這種場景也會形成消息調度不及時,進而致使響應超時問題。(後面會在 ANR 實例分析中對其進行介紹)

發生 ANR 時主線程消息調度示意圖以下:

應用進程或系統(包括其它進程)負載太重

除了上面列舉了一些主線程消息耗時嚴重或者消息過多,致使的消息調度不及時的可能引發的問題以外,還有一種咱們在線上常常遇到的場景,那就是進程或系統自己負載很重,如高 CPU,高 IO,低內存(應用內內存抖動頻繁 GC,系統內存回收)等等。這種狀況出現以後,也很致使應用或總體系統性能變差,最終致使一系列超時問題

針對這種狀況,具體到主線程消息調度上表現來看,就是不少消息耗時都比較嚴重,並且每次消息調度統計的 Wall Duration(絕對時間:包括正常調度和等待,休眠時間)和 CPU Duration(絕對時間:只包括 CPU 執行時間)相差很大,若是出現這種狀況咱們則認爲系統負載可能發生了異常,須要藉助系統信息進一步對比分析。這種狀況不只影響當前應用,也會影響其餘應用乃至系統自身。

發生 ANR 時主線程消息調度示意圖以下:

總結

經過上面的介紹,咱們介紹了 ANR 的設計原理及工做過程,對影響 ANR 的因素和分類也有了進一步認識。從歸類上咱們能夠發現,影響 ANR 的場景會有不少種,甚至不少時候都是層層疊加致使,因此能夠借用一句話來形容:「當 ANR 發生時,沒有一個消息是無辜的」

後續

依靠系統現有的監控能力,並不能直觀的體現上面列舉的衆多場景,更沒法直觀告訴咱們 ANR 發生前主線程調度狀況。僅僅依靠 ANR 時獲取系統及 Top 進程的相關信息和一些 Log 日誌,不少數時候只能幫咱們完成第一階段的定位,如系統負載太重,主線程過於繁忙等結論。卻沒法更進一步深刻分析和解決問題,尤爲是一些線下難以復現的問題。

對於咱們每一個人來講,工做的目標不只僅是定位方向,更重要的是解決問題。那麼怎麼才能更好的解決上述系統監控能力不完善以及應用側信息盲區的問題呢?這就是咱們下一期要重點介紹的「監控工具」,一個優秀的工具,不只能夠幫助咱們在解決常規問題時達到一槌定音的效果,在面對更加複雜隱蔽的問題時,也能爲咱們打開視野,提供更多方向,下週的文章咱們就去看看它是如何設計及運用的。

Android 平臺架構團隊

咱們是字節跳動 Android 平臺架構團隊,以服務今日頭條爲主,面向 GIP,同時服務公司其餘產品,在產品性能穩定性等用戶體驗,研發流程,架構方向上持續優化和探索,知足產品快速迭代的同時,保持較高的用戶體驗。

若是你對技術充滿熱情,想要迎接更大的挑戰和舞臺,歡迎加入咱們,北京,深圳均有崗位,感興趣發送郵箱:tech@bytedance.com ,郵件標題:姓名 - GIP - Android 平臺架構


歡迎關注「 字節跳動技術團隊

簡歷投遞聯繫郵箱「 tech@bytedance.com

相關文章
相關標籤/搜索