鴻蒙開源第三方組件—ANR異常監測組件 ANR-WatchDog-ohos
前言
基于安卓平臺的消息彈框組件ANR-WatchDog(https://github.com/SalomonBrys/ANR-WatchDog),實現鴻蒙化遷移和重構。代碼已經開源到(https://gitee.com/isrc_ohos/anr-watch-dog-ohos),歡迎各位下載使用并提出寶貴意見!
背景
ANR-WatchDog-ohos是一個監測組件,可以監測鴻蒙應用的ANR(Application Not Response-應用程序無響應)錯誤,并能及時拋出異常。在此組件被移植成功之前,鴻蒙應用程序是無法捕獲和報告ANR錯誤的,調查ANR的唯一方法是查看/data/anr/traces.txt文件。因此ANR-WatchDog-ohos為ANR捕獲過程提供了更好的交互性、便捷性以及可視化的效果,同時也提升了程序的健壯性。
組件效果展示
1、組件應用的界面介紹
為了更好的向開發者展示組件的運行效果,先來了解一下組件應用中各按鈕的含義。在圖1中,藍色框內是ANR的監測模式設置按鈕,紅色框內的是ANR模擬按鈕。下面具體解釋各按鈕的含義:
- Min ANR duration:阻塞響應時間按鈕。開發者通過點擊按鈕設置阻塞響應時間為2秒、4秒或6秒,即應用阻塞2秒、4秒或6秒后,執行特定的響應行為。
- Report mode:報告模式按鈕。開發者通過點擊按鈕設置ANR發生時,HiLog中輸出錯誤報告的模式:All Threads表示輸出每個線程的錯誤日志;Main thread only 表示只輸出主線程的錯誤日志;Filtered表示只輸出符合特定過濾條件的線程的錯誤日志。
- Behaviour:響應行為按鈕。開發者通過點擊按鈕設置ANR發生時應用的響應行為:Crash表示應用閃退;Silent表示開發者自定義應用的響應行為。
- Thread Sleep:可以模擬主線程休眠。
- Infinite loop:可以模擬主線程無限循環。
- Dead lock:可以模擬主線程死鎖。

圖1 ANR-WatchDog-ohos組件應用的界面介紹
2、組件運行效果展示
通過點擊圖1紅色框內三個不同的按鈕,可以看到三種不同的ANR發生時組件的運行效果。為了更清楚的展現檢測模式的作用,我們給每個ANR模擬按鈕設置不同的檢測模式。下面對組件的運行效果進行詳細描述:
1、線程休眠
ANR監測模式:阻塞響應時間為2秒,報告模式為All Threads、響應行為Crash。
點擊Thread Sleep按鈕,啟動主線程休眠后,ANR-WatchDog-ohos組件監測到程序在2秒內一直無響應,于是觸發應用閃退,并通過HiLog報告所有線程的ANR詳情,其模式設置和執行效果如圖2所示。
在報告中,可以根據“Caused by”后面的堆棧信息追蹤查看線程休眠的具體原因,如圖3所示。

圖2 線程休眠設置流程和執行效果

圖3 監測線程休眠后閃退輸出的HiLog信息
2、線程無限循環
ANR監測模式:阻塞響應時間為4秒,報告模式為All Threads、響應行為Crash。
點擊Infinite loop按鈕,啟動線程無限循環后,ANR-WatchDog-ohos組件監測到程序在4秒內一直無響應,于是觸發應用閃退,并通過HiLog報告主線程的ANR錯誤詳情,其監測模式設置和執行效果如圖4所示,HiLog報告主線程的ANR詳情如圖5所示。

圖4 線程無限循環設置流程和執行效果

圖5 監測線程無限循環后閃退輸出的HiLog信息
3、線程死鎖
ANR監測模式:阻塞響應時間為6秒,報告模式為Filtered(只報告以“APP:”為前綴的線程)、響應行為Crash。
點擊Dead lock按鈕,啟動線程死鎖后,ANR-WatchDog-ohos組件監測到程序在6秒內一直無響應,于是觸發應用閃退,并通過HiLog報告以“APP:”為前綴線程的ANR錯誤詳情,其監測模式設置和執行效果如圖6所示,HiLog報告主線程的ANR詳情如圖7所示。

圖6 線程死鎖設置流程和執行效果

圖7 監測線程死鎖后閃退輸出的HiLog信息
值得注意的是:無論在哪種ANR類型下,只要將Behaviour設置為Silent,應用遇到ANR時的響應行為都需要開發者自定義。例如此處我們定義:應用遇到ANR的情況時,通過HiLog打印出ANR-Watchdog-Demo的tag,如圖7所示:

圖8 Silent行為下不閃退只輸出HiLog信息
Sample解析
ANR-WatchDog-ohos組件能夠監測多種類型的ANR錯誤,及時捕捉并觸發相應的響應行為。下面將具體講解ANR-WatchDog-ohos組件的使用方法,共分為7個步驟,其中步驟1至步驟2在MyApplication文件中進行,步驟3至步驟7在MainAbility文件中進行:
步驟1. 導入相關類并實例化類對象。
步驟2. 設置ANRListener監聽。
步驟3. 模擬主線程休眠、無限循環和死鎖。
步驟4. 創建xml文件。
步驟5. 設置整體布局,并實例化MyApplication對象。
步驟6. 設置ANR檢測模式Button的點擊事件。
步驟7. 設置ANR模擬Button的點擊事件。
(1)導入相關類并實例化類對象
在MyApplication文件中,導入ANRError類和ANRWatchDog類并實例化ANRWatchDog類的對象,設置默認的阻塞響應時間Min ANR duration為2000毫秒(2秒)。其中,ANRWatchDog類的作用是檢測ANR的情況是否出現,ANRError類的作用是拋出錯誤信息,即正在運行線程的堆棧追蹤信息。
- //導入ANRError類和ANRWatchDog類
- import com.github.anrwatchdog.ANRError;
- import com.github.anrwatchdog.ANRWatchDog;
- //實例化ANRWatchDog類對象
- ANRWatchDog anrWatchDog = new ANRWatchDog(2000);//設置阻塞響應時間為2000毫秒(2秒)
(2)設置ANRListener監聽
當響應行為按鈕設置為Crash:
由于MyApplication類繼承了AbilityPackage類,因此需要重寫onInitialize()方法。在onInitialize()方法中,需要調用ANRWatchDog類的setANRListener()方法,為應用設置ANR監聽,其中onAppNotResponding()方法用于在上述監聽中設置應用的ANR響應行為,此處設置ANR情況發生時,應用crash并拋出異常。當需要提前或推遲報告ANR錯誤或者執行響應行為時,在onInitialize()方法中,可以通過調用ANRWatchDog類的setANRInterceptor()方法設置攔截器,實現在給定的響應時間內對異常或其他自定義的響應行為進行攔截。
- //重寫onInitialize()方法
- @Override
- public void onInitialize() {
- super.onInitialize();
- //設置ANRListener監聽
- anrWatchDog.setANRListener(new ANRWatchDog.ANRListener() {
- @Override//設置監測到ANR錯誤后的具體響應行為
- public void onAppNotResponding(ANRError error) {
- ...
- throw error;//直接拋出錯誤異常,程序閃退 }
- })
- .setANRInterceptor(new ANRWatchDog.ANRInterceptor() {
- @Override//定義攔截器來決定是否提前或推遲
- public long intercept(long duration) {...}
- });
- anrWatchDog.setIgnoreDebugger(true).start();//在debug的情況下也能拋出ANR異常
- }
當響應行為按鈕設置為Silent:
此時需要設置ANRListener 類的對象為final 對象,對象內部的內容可變,但是引用不會變。我們定義:線程阻塞后程序不閃退,而是打印ANR-Watchdog-Demo的tag,因此在重寫ANRWatchDog類的onAppNotResponding()方法時,只需要自定義相應的Hilog報告即可,不需要拋出異常。
- final ANRWatchDog.ANRListener silentListener = new ANRWatchDog.ANRListener() {
- @Override//重寫setANRListner()方法
- public void onAppNotResponding(ANRError error) {//自定義ANRListener回調
- HiLog.error(new HiLogLabel(HiLog.LOG_APP,0,"ANR-Watchdog-Demo"), "", error);
- }
- };
(3)模擬線程休眠、線程無限循環和線程死鎖
為了使ANR-WatchDog-ohos能監測到如線程休眠、線程無限循環和線程死鎖不同情況下的ANR,需要分別設置函數,模擬這三種情況。
線程休眠
- private static void Sleep() {//模擬線程休眠的情況
- try {
- Thread.sleep(8 * 1000);//線程休眠8秒后釋放鎖
- }
- catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
線程無限循環
- private static void InfiniteLoop() {//模擬線程無限循環的情況
- int i = 0;
- while (true) {//判斷條件恒為true,則無限循環
- i++;
- }
- }
線程死鎖
- private void lock(){//模擬線程死鎖的情況
- new Thread(){
- @Override
- public void run(){
- synchronized (MainAbility.this){//線程占用鎖
- try{
- Thread.sleep(60000);//休眠60秒后釋放鎖
- }
- ...}
- }.start();
- synchronized (MainAbility.this){//主線程也同時占用鎖
- HiLog.info(new HiLogLabel(HiLog.LOG_APP,0,"ANR-Failed"),"主線程也申請鎖");
(4)創建xml文件
在ability_main.xml中創建顯示文件,最主要的部分是圖1藍框中3個模式設置按鈕和紅框中3個ANR類型的按鈕。
- <DirectionalLayout//創建整體布局
- xmlns:ohos="http://schemas.huawei.com/res/ohos"
- ohos:height="match_parent"
- ohos:width="match_parent"
- ohos:orientation="vertical">
- ...
- //圖1紅框中的按鈕
- <Button //阻塞響應時間按鈕
- ohos:id="$+id:minAnrDuration"
- ohos:width="match_content"
- ohos:height="match_content"
- ohos:text="2s"
- ohos:text_size="150"/>
- ... //報告模式按鈕和響應行為按鈕同上
- //圖1紅框中的按鈕
- <Button //線程休眠按鈕
- ohos:id="$+id:threadSleep"
- ohos:left_margin="24"
- ohos:width="match_content"
- ohos:height="match_content"
- ohos:text="$string:threadsleep"
- .../>
- ... //線程無限循環按鈕和死鎖按鈕同上
- </DirectionalLayout>
(5)設置整體布局,并實例化MyApplication對象
通過setUIContent()方法加載上一步設置好的xml文件作為整體顯示布局,實例化MyApplication對象為后續設置各按鈕的點擊事件做準備。
- setUIContent(ResourceTable.Layout_ability_main);//加載UI布局
- final MyApplication application = (MyApplication) getAbilityPackage();//實例化
(6)設置ANR檢測模式Button的點擊事件
本步驟需要阻塞響應時間、報告模式和響應行按鈕的點擊事件。
阻塞響應時間Button
為實現每點擊按鈕一次就切換一種阻塞響應時間,需要用公式將變量application.duration控制在2秒、4秒和6秒之間。application.duration的初始值為4,每點擊一次按鈕,將application.duration整除6的余數加上2的值重新復制給application.duration,可以實現上述切換效果。
- minAnrDurationButton.setClickedListener(new Component.ClickedListener() {
- @Override//重寫onClick()方法
- public void onClick(Component component) {
- application.duration = application.duration % 6 + 2;//得到整除6的余數加2
- minAnrDurationButton.setText(application.duration + " seconds");
- }
- });
報告模式Button
為實現每點擊按鈕一次就切換一種報告模式,需要用公式將變量mode控制在0、1、2這三個值中。0表示All Threads;1表示Main thread only;2表示Filtered。mode初始值為0,所以第一次點擊后mode值變為1,通過setReportMainThreadOnly()方法設置為只報告主線程,其他情況與上述類似。
- reportModeButton.setClickedListener(new Component.ClickedListener() {
- @Override//重寫onClick()方法
- public void onClick(Component component) {
- mode = (mode + 1) % 3;//得到mode加1并整除3后的余數
- switch (mode) {
- case 0:
- ...//所有線程
- application.anrWatchDog.setReportAllThreads();break ;
- case 1:
- ...//只有主線程
- application.anrWatchDog.setReportMainThreadOnly();break ;
- case 2:
- ...//過濾以“APP:”為前綴的線程
- application.anrWatchDog.setReportThreadNamePrefix("APP:");break ;
- }
- }
- });
響應行為Button
crash變量是ANR響應行為的標志位,為實現每點擊按鈕一次就切換一種響應行為,需要判斷crash變量是否為true。如果crash變量為true,則說明在監測到ANR錯誤后應用直接閃退,需要通過setANRListener()方法調用步驟(2)中響應行為為Crash時的onAppNotResponding()方法;反之,則說明開發者自定義了監測到ANR錯誤后應用的響應行為,需要通過setANRListener()方法調用步驟(2)中的響應行為為Silent時的onAppNotResponding()方法。
- behaviourButton.setClickedListener(new Component.ClickedListener() {
- @Override//重寫onClick()方法
- public void onClick(Component component) {
- crash = !crash;每次點擊更改crash的布爾類型
- if (crash) {//crash為true
- behaviourButton.setText("Crash");
- application.anrWatchDog.setANRListener(null);//無需設置回調
- } else {//crash不為true
- behaviourButton.setText("Silent");//自定義ANRListener回調
- application.anrWatchDog.setANRListener(application.silentListener);
- }
- }
(7)設置ANR模擬Button的點擊事件
最后需要設置線程休眠、線程無限循環和線程死鎖按鈕的點擊事件。此處以線程休眠按鈕為例,只需在對應的onClick()方法中調用各自的模擬函數即可,其他兩種情況同理。
- findComponentById(ResourceTable.Id_threadSleep).setClickedListener(new Component.ClickedListener() {//線程休眠Button的click點擊事件
- @Override
- public void onClick(Component component) {//重寫onClick()方法
- Sleep();//調用模擬線程休眠的函數
- }
- });
Library解析
Library包含兩個重要的類,即ANRWatchDog和ANRError,它們向開發者提供使用ANR-WatchDog-ohos組件監測并處理ANR錯誤的具體執行方法,本節將分別講解ANRWatchDog類和ANRError類的內部邏輯。
1、ANRWatchDog類
(1)構造方法阻塞響應時間
ANRWatchDog類繼承自Thread類,其實質是一個線程,因此根據線程的特性,我們可以隨時將其中斷。ANRWatchDog類提供了兩個構造方法,使用第二個帶參的構造方法,開發者能夠對阻塞響應時間進行設置,使用第一個不帶參的構造方法,阻塞響應時間默認配置為5000ms。
- //構造方法一
- public ANRWatchDog() {
- this(DEFAULT_ANR_TIMEOUT);//使用默認的阻塞響應時間5000ms
- }
- //構造方法二
- public ANRWatchDog(int timeoutInterval) {
- super();
- _timeoutInterval = timeoutInterval;//自定義阻塞響應時間timeoutInterval
- }
(2)任務單元_ticker判斷主線程是否阻塞

圖9 監測主線程是否阻塞的原理
在ANRWatchDog類中,監測主線程是否阻塞的具體原理流程如圖9,其核心是向主線程拋出一個Runnable類型的任務單元_ticker,然后判斷其在特定時間內是否被主線程處理,若_ticker被處理,說明主線程未阻塞,需要進行循環判斷。若未被處理,說明主線程阻塞,需要向開發者發送ANR錯誤信息。
變量_tick標志著_ticker是否被處理,其初始值為0,并且是volatile類型的,這個類型的好處是能夠保證此變量在被不同線程操作時的可見性,即如果某線程修改了此變量的值,那么新值對其他線程來說是立即可見的。在未執行在_ticker之前,_tick的值為阻塞響應時間,執行了_ticker后,_tick的值會被重置為0,因此只需要判斷_tick值是否被重置為0即可獲知_ticker是否被處理。
- private volatile long _tick = 0; //用于標志_ticker是否被處理
- private volatile boolean _reported = false;
- private final Runnable _ticker = new Runnable() {
- @Override public void run() {//_ticker處理線程
- _tick = 0;//重置為初始值0,表示_ticker被處理
- _reported = false;
- }
- };
復制 在ANRWatchDog類的run()方法中,先通過_tick值判斷_ticker是否被發送給主線程,如果_tick的值為0則有兩種情況,一種是_tick的初始值為0,_ticker從未被發送給主線程;另一種是_ticker完成了一次或多次發送周期,且均被主線程處理,_tick被重置為0。在上述兩種情況下,需要將_tick值加上一段阻塞響應時間后重新發送給主線程。
- @Override
- public void run() {//ANRWatchDog類的執行過程
- setName("|ANR-WatchDog|");
- long interval = _timeoutInterval;
- while (!isInterrupted()) {
- boolean needPost = _tick == 0;//將“_tick是初始值0”賦給needPost
- _tick += interval;//_tick值加一個阻塞響應時間
- if (needPost) {//判斷_tick是否為0
- _uiHandler.postTask(_ticker);//發送_ticker給主線程
- }
- ...}
如果_tick的值不為0,此時ANRWatchDog線程需要休眠一個阻塞響應時間(對應圖的1藍框中的Min ANR duration)。休眠結束后,繼續根據_tick的值可以判斷_ticker是否被處理,如果_tick被重置為0,則說明主線程處理了_ticker,主線程未阻塞;反之則說明主線程沒有處理_ticker,主線程阻塞,需要通過ANRError類拋出錯誤信息(具體操作間ANRError類的介紹),并返回一個ANRError類的實例。
- try {
- Thread.sleep(interval);//ANRWatchDog線程休眠一個阻塞響應時間
- } catch (InterruptedException e) {
- _interruptionListener.onInterrupted(e);
- return ;
- }
- if (_tick != 0 && !_reported) {//如果主線程沒有處理_ticker,則主線程阻塞
- ...
- final ANRError error;//聲明ANRError類
- if (_namePrefix != null) {//調用ANRError類的New()方法
- error = ANRError.New(_tick, _namePrefix, _logThreadsWithoutStackTrace);
- } else {//調用ANRError類NewMainOnly()方法
- error = ANRError.NewMainOnly(_tick);
- }
- }
隨后,調用ANRListener類onAppNotResponding()方法設置主線程阻塞后的響應行為(對應圖1藍框中的Behaviour)。
- _anrListener.onAppNotResponding(error); //響應行為設置
2、 ANRError類
ANRError類繼承自Error類,主要用于拋出錯誤信息,其有兩個重要的方法,分別是New()方法和NewMainOnly()方法。以下兩段代碼分別展示了兩個方法的具體邏輯。通過對比可發現這兩個方法的處理過程其實是類似的,核心都是先通過getMainEventRunner()方法獲取主線程mainThread ;再通過主線程得到堆棧信息mainStackTrace ,最后以mainThread和mainStackTrace作為參數實例化ANRError對象,并將該對象作為函數返回值。
NewMainOnly()方法
- static ANRError NewMainOnly(long duration) {
- final Thread mainThread = //通過getMainEventRunner()方法獲取到主線程findThread(EventRunner.getMainEventRunner().getThreadId());
- //獲取堆棧信息
- final StackTraceElement[] mainStackTrace = mainThread.getStackTrace();
- return new ANRError(new $(getThreadTitle(mainThread), mainStackTrace).new _Thread(null), duration);//返回重新構造的ANRError實例
- }
New()方法
- static ANRError New(long duration, String prefix, boolean logThreadsWithoutStackTrace) {
- final Thread mainThread = //通過getMainEventRunner()方法獲取到主線程findThread(EventRunner.getMainEventRunner().getThreadId());
- final Map<Thread, StackTraceElement[]> stackTraces = new TreeMap<Thread, StackTraceElement[]>(new Comparator<Thread>() {@Override...});//獲取堆棧信息
- ...
- for (Map.Entry<Thread, StackTraceElement[]> entry : stackTraces.entrySet())
- tst = new $(getThreadTitle(entry.getKey()), entry.getValue()).new _Thread(tst);//重新構造ANRError實例
- return new ANRError(tst, duration);//返回重新構造的ANRError實例
- }