鸿蒙开源第三方组件—ANR异常监测组件 ANR-WatchDog-ohos

系统
ANR-WatchDog-ohos是一个监测组件,可以监测鸿蒙应用的ANR(Application Not Response-应用程序无响应)错误,并能及时抛出异常。

[[416599]]

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

前言

基于安卓平台的消息弹框组件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:可以模拟主线程死锁。
鸿蒙开源第三方组件——ANR异常监测组件 ANR-WatchDog-ohos-鸿蒙HarmonyOS技术社区

图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所示。

鸿蒙开源第三方组件——ANR异常监测组件 ANR-WatchDog-ohos-鸿蒙HarmonyOS技术社区

图2 线程休眠设置流程和执行效果

鸿蒙开源第三方组件——ANR异常监测组件 ANR-WatchDog-ohos-鸿蒙HarmonyOS技术社区

图3 监测线程休眠后闪退输出的HiLog信息

2、线程无限循环

ANR监测模式:阻塞响应时间为4秒,报告模式为All Threads、响应行为Crash。

点击Infinite loop按钮,启动线程无限循环后,ANR-WatchDog-ohos组件监测到程序在4秒内一直无响应,于是触发应用闪退,并通过HiLog报告主线程的ANR错误详情,其监测模式设置和执行效果如图4所示,HiLog报告主线程的ANR详情如图5所示。

鸿蒙开源第三方组件——ANR异常监测组件 ANR-WatchDog-ohos-鸿蒙HarmonyOS技术社区

图4 线程无限循环设置流程和执行效果

鸿蒙开源第三方组件——ANR异常监测组件 ANR-WatchDog-ohos-鸿蒙HarmonyOS技术社区

图5 监测线程无限循环后闪退输出的HiLog信息

3、线程死锁

ANR监测模式:阻塞响应时间为6秒,报告模式为Filtered(只报告以“APP:”为前缀的线程)、响应行为Crash。

点击Dead lock按钮,启动线程死锁后,ANR-WatchDog-ohos组件监测到程序在6秒内一直无响应,于是触发应用闪退,并通过HiLog报告以“APP:”为前缀线程的ANR错误详情,其监测模式设置和执行效果如图6所示,HiLog报告主线程的ANR详情如图7所示。

鸿蒙开源第三方组件——ANR异常监测组件 ANR-WatchDog-ohos-鸿蒙HarmonyOS技术社区

图6 线程死锁设置流程和执行效果

鸿蒙开源第三方组件——ANR异常监测组件 ANR-WatchDog-ohos-鸿蒙HarmonyOS技术社区

图7 监测线程死锁后闪退输出的HiLog信息

值得注意的是:无论在哪种ANR类型下,只要将Behaviour设置为Silent,应用遇到ANR时的响应行为都需要开发者自定义。例如此处我们定义:应用遇到ANR的情况时,通过HiLog打印出ANR-Watchdog-Demo的tag,如图7所示:

鸿蒙开源第三方组件——ANR异常监测组件 ANR-WatchDog-ohos-鸿蒙HarmonyOS技术社区

图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类的作用是抛出错误信息,即正在运行线程的堆栈追踪信息。

  1. //导入ANRError类和ANRWatchDog类 
  2. import com.github.anrwatchdog.ANRError; 
  3. import com.github.anrwatchdog.ANRWatchDog; 
  4. //实例化ANRWatchDog类对象 
  5. 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()方法设置拦截器,实现在给定的响应时间内对异常或其他自定义的响应行为进行拦截。

  1. //重写onInitialize()方法 
  2. @Override 
  3. public void onInitialize() { 
  4.     super.onInitialize(); 
  5.     //设置ANRListener监听 
  6.     anrWatchDog.setANRListener(new ANRWatchDog.ANRListener() {  
  7.                    @Override//设置监测到ANR错误后的具体响应行为 
  8.                    public void onAppNotResponding(ANRError error) { 
  9.                        ... 
  10.                     throw error;//直接抛出错误异常,程序闪退  } 
  11.             }) 
  12.             .setANRInterceptor(new ANRWatchDog.ANRInterceptor() { 
  13.                 @Override//定义拦截器来决定是否提前或推迟 
  14.                 public long intercept(long duration) {...} 
  15.             }); 
  16.     anrWatchDog.setIgnoreDebugger(true).start();//在debug的情况下也能抛出ANR异常 

当响应行为按钮设置为Silent:

此时需要设置ANRListener 类的对象为final 对象,对象内部的内容可变,但是引用不会变。我们定义:线程阻塞后程序不闪退,而是打印ANR-Watchdog-Demo的tag,因此在重写ANRWatchDog类的onAppNotResponding()方法时,只需要自定义相应的Hilog报告即可,不需要抛出异常。

  1. final ANRWatchDog.ANRListener silentListener = new ANRWatchDog.ANRListener() { 
  2.     @Override//重写setANRListner()方法 
  3.     public void onAppNotResponding(ANRError error) {//自定义ANRListener回调 
  4.         HiLog.error(new HiLogLabel(HiLog.LOG_APP,0,"ANR-Watchdog-Demo"), "", error); 
  5.     } 
  6. }; 

(3)模拟线程休眠、线程无限循环和线程死锁

为了使ANR-WatchDog-ohos能监测到如线程休眠、线程无限循环和线程死锁不同情况下的ANR,需要分别设置函数,模拟这三种情况。

线程休眠

  1. private static void Sleep() {//模拟线程休眠的情况 
  2.     try { 
  3.         Thread.sleep(8 * 1000);//线程休眠8秒后释放锁 
  4.     } 
  5.     catch (InterruptedException e) { 
  6.         e.printStackTrace(); 
  7.     } 

线程无限循环

  1. private static void InfiniteLoop() {//模拟线程无限循环的情况 
  2.     int i = 0; 
  3.     while (true) {//判断条件恒为true,则无限循环 
  4.         i++; 
  5.     } 

线程死锁

  1. private  void  lock(){//模拟线程死锁的情况 
  2.     new Thread(){ 
  3.         @Override 
  4.         public void run(){ 
  5.             synchronized (MainAbility.this){//线程占用锁 
  6.                 try{ 
  7.                     Thread.sleep(60000);//休眠60秒后释放锁 
  8.                 } 
  9.             ...} 
  10.     }.start(); 
  11.     synchronized (MainAbility.this){//主线程也同时占用锁 
  12.         HiLog.info(new HiLogLabel(HiLog.LOG_APP,0,"ANR-Failed"),"主线程也申请锁"); 

(4)创建xml文件

在ability_main.xml中创建显示文件,最主要的部分是图1蓝框中3个模式设置按钮和红框中3个ANR类型的按钮。

  1. <DirectionalLayout//创建整体布局 
  2.     xmlns:ohos="http://schemas.huawei.com/res/ohos" 
  3.     ohos:height="match_parent" 
  4.     ohos:width="match_parent" 
  5.     ohos:orientation="vertical"
  6.     ... 
  7.     //图1红框中的按钮 
  8.     <Button         //阻塞响应时间按钮 
  9.         ohos:id="$+id:minAnrDuration" 
  10.         ohos:width="match_content" 
  11.         ohos:height="match_content" 
  12.         ohos:text="2s" 
  13.    ohos:text_size="150"/> 
  14.         ...      //报告模式按钮和响应行为按钮同上 
  15. //图1红框中的按钮 
  16.     <Button     //线程休眠按钮 
  17.     ohos:id="$+id:threadSleep" 
  18.         ohos:left_margin="24" 
  19.         ohos:width="match_content" 
  20.         ohos:height="match_content" 
  21.     ohos:text="$string:threadsleep" 
  22.     .../> 
  23.          ...    //线程无限循环按钮和死锁按钮同上 
  24. </DirectionalLayout> 

(5)设置整体布局,并实例化MyApplication对象

通过setUIContent()方法加载上一步设置好的xml文件作为整体显示布局,实例化MyApplication对象为后续设置各按钮的点击事件做准备。

  1. setUIContent(ResourceTable.Layout_ability_main);//加载UI布局 
  2. final MyApplication application = (MyApplication) getAbilityPackage();//实例化 

(6)设置ANR检测模式Button的点击事件

本步骤需要阻塞响应时间、报告模式和响应行按钮的点击事件。

阻塞响应时间Button

为实现每点击按钮一次就切换一种阻塞响应时间,需要用公式将变量application.duration控制在2秒、4秒和6秒之间。application.duration的初始值为4,每点击一次按钮,将application.duration整除6的余数加上2的值重新复制给application.duration,可以实现上述切换效果。

  1. minAnrDurationButton.setClickedListener(new Component.ClickedListener() { 
  2.     @Override//重写onClick()方法 
  3.     public void onClick(Component component) { 
  4.         application.duration = application.duration % 6 + 2;//得到整除6的余数加2 
  5.         minAnrDurationButton.setText(application.duration + " seconds"); 
  6.     } 
  7. }); 

报告模式Button

为实现每点击按钮一次就切换一种报告模式,需要用公式将变量mode控制在0、1、2这三个值中。0表示All Threads;1表示Main thread only;2表示Filtered。mode初始值为0,所以第一次点击后mode值变为1,通过setReportMainThreadOnly()方法设置为只报告主线程,其他情况与上述类似。

  1. reportModeButton.setClickedListener(new Component.ClickedListener() { 
  2.     @Override//重写onClick()方法 
  3.     public void onClick(Component component) { 
  4.         mode = (mode + 1) % 3;//得到mode加1并整除3后的余数 
  5.         switch (mode) { 
  6.             case 0: 
  7.                 ...//所有线程 
  8.                 application.anrWatchDog.setReportAllThreads();break ; 
  9.             case 1: 
  10.                 ...//只有主线程 
  11.                 application.anrWatchDog.setReportMainThreadOnly();break ; 
  12.             case 2: 
  13.                 ...//过滤以“APP:”为前缀的线程 
  14.               application.anrWatchDog.setReportThreadNamePrefix("APP:");break ; 
  15.         } 
  16.     } 
  17. }); 

响应行为Button

crash变量是ANR响应行为的标志位,为实现每点击按钮一次就切换一种响应行为,需要判断crash变量是否为true。如果crash变量为true,则说明在监测到ANR错误后应用直接闪退,需要通过setANRListener()方法调用步骤(2)中响应行为为Crash时的onAppNotResponding()方法;反之,则说明开发者自定义了监测到ANR错误后应用的响应行为,需要通过setANRListener()方法调用步骤(2)中的响应行为为Silent时的onAppNotResponding()方法。

  1. behaviourButton.setClickedListener(new Component.ClickedListener() { 
  2.     @Override//重写onClick()方法 
  3.     public void onClick(Component component) { 
  4.         crash = !crash;每次点击更改crash的布尔类型 
  5.         if (crash) {//crash为true 
  6.             behaviourButton.setText("Crash"); 
  7.             application.anrWatchDog.setANRListener(null);//无需设置回调 
  8.         } else {//crash不为true 
  9.             behaviourButton.setText("Silent");//自定义ANRListener回调 
  10.            application.anrWatchDog.setANRListener(application.silentListener); 
  11.         } 
  12.     } 

(7)设置ANR模拟Button的点击事件

最后需要设置线程休眠、线程无限循环和线程死锁按钮的点击事件。此处以线程休眠按钮为例,只需在对应的onClick()方法中调用各自的模拟函数即可,其他两种情况同理。

  1. findComponentById(ResourceTable.Id_threadSleep).setClickedListener(new Component.ClickedListener() {//线程休眠Button的click点击事件 
  2.     @Override 
  3.     public void onClick(Component component) {//重写onClick()方法 
  4.         Sleep();//调用模拟线程休眠的函数 
  5.     } 
  6. }); 

Library解析

Library包含两个重要的类,即ANRWatchDog和ANRError,它们向开发者提供使用ANR-WatchDog-ohos组件监测并处理ANR错误的具体执行方法,本节将分别讲解ANRWatchDog类和ANRError类的内部逻辑。

1、ANRWatchDog类

(1)构造方法阻塞响应时间

ANRWatchDog类继承自Thread类,其实质是一个线程,因此根据线程的特性,我们可以随时将其中断。ANRWatchDog类提供了两个构造方法,使用第二个带参的构造方法,开发者能够对阻塞响应时间进行设置,使用第一个不带参的构造方法,阻塞响应时间默认配置为5000ms。

  1. //构造方法一 
  2. public ANRWatchDog() { 
  3.     this(DEFAULT_ANR_TIMEOUT);//使用默认的阻塞响应时间5000ms 
  4. //构造方法二 
  5. public ANRWatchDog(int timeoutInterval) { 
  6.     super(); 
  7.     _timeoutInterval = timeoutInterval;//自定义阻塞响应时间timeoutInterval 

(2)任务单元_ticker判断主线程是否阻塞

鸿蒙开源第三方组件——ANR异常监测组件 ANR-WatchDog-ohos-鸿蒙HarmonyOS技术社区

图9 监测主线程是否阻塞的原理

在ANRWatchDog类中,监测主线程是否阻塞的具体原理流程如图9,其核心是向主线程抛出一个Runnable类型的任务单元_ticker,然后判断其在特定时间内是否被主线程处理,若_ticker被处理,说明主线程未阻塞,需要进行循环判断。若未被处理,说明主线程阻塞,需要向开发者发送ANR错误信息。

变量_tick标志着_ticker是否被处理,其初始值为0,并且是volatile类型的,这个类型的好处是能够保证此变量在被不同线程操作时的可见性,即如果某线程修改了此变量的值,那么新值对其他线程来说是立即可见的。在未执行在_ticker之前,_tick的值为阻塞响应时间,执行了_ticker后,_tick的值会被重置为0,因此只需要判断_tick值是否被重置为0即可获知_ticker是否被处理。

  1. private volatile long _tick = 0; //用于标志_ticker是否被处理 
  2. private volatile boolean _reported = false
  3. private final Runnable _ticker = new Runnable() { 
  4.     @Override public void run() {//_ticker处理线程 
  5.         _tick = 0;//重置为初始值0,表示_ticker被处理 
  6.         _reported = false
  7.     } 
  8. }; 

复制 在ANRWatchDog类的run()方法中,先通过_tick值判断_ticker是否被发送给主线程,如果_tick的值为0则有两种情况,一种是_tick的初始值为0,_ticker从未被发送给主线程;另一种是_ticker完成了一次或多次发送周期,且均被主线程处理,_tick被重置为0。在上述两种情况下,需要将_tick值加上一段阻塞响应时间后重新发送给主线程。

  1. @Override 
  2. public void run() {//ANRWatchDog类的执行过程 
  3.     setName("|ANR-WatchDog|"); 
  4.     long interval = _timeoutInterval; 
  5.     while (!isInterrupted()) { 
  6.         boolean needPost = _tick == 0;//将“_tick是初始值0”赋给needPost 
  7.         _tick += interval;//_tick值加一个阻塞响应时间 
  8.         if (needPost) {//判断_tick是否为0 
  9.             _uiHandler.postTask(_ticker);//发送_ticker给主线程 
  10.         } 
  11. ...}  

如果_tick的值不为0,此时ANRWatchDog线程需要休眠一个阻塞响应时间(对应图的1蓝框中的Min ANR duration)。休眠结束后,继续根据_tick的值可以判断_ticker是否被处理,如果_tick被重置为0,则说明主线程处理了_ticker,主线程未阻塞;反之则说明主线程没有处理_ticker,主线程阻塞,需要通过ANRError类抛出错误信息(具体操作间ANRError类的介绍),并返回一个ANRError类的实例。

  1. try { 
  2.     Thread.sleep(interval);//ANRWatchDog线程休眠一个阻塞响应时间 
  3. } catch (InterruptedException e) { 
  4.     _interruptionListener.onInterrupted(e); 
  5.     return ; 
  6. if (_tick != 0 && !_reported) {//如果主线程没有处理_ticker,则主线程阻塞 
  7.     ... 
  8.     final ANRError error;//声明ANRError类 
  9.     if (_namePrefix != null) {//调用ANRError类的New()方法 
  10.             error = ANRError.New(_tick, _namePrefix, _logThreadsWithoutStackTrace); 
  11.     } else {//调用ANRError类NewMainOnly()方法 
  12.             error = ANRError.NewMainOnly(_tick); 
  13.     } 

随后,调用ANRListener类onAppNotResponding()方法设置主线程阻塞后的响应行为(对应图1蓝框中的Behaviour)。

  1. _anrListener.onAppNotResponding(error);  //响应行为设置 

2、 ANRError类

ANRError类继承自Error类,主要用于抛出错误信息,其有两个重要的方法,分别是New()方法和NewMainOnly()方法。以下两段代码分别展示了两个方法的具体逻辑。通过对比可发现这两个方法的处理过程其实是类似的,核心都是先通过getMainEventRunner()方法获取主线程mainThread ;再通过主线程得到堆栈信息mainStackTrace ,最后以mainThread和mainStackTrace作为参数实例化ANRError对象,并将该对象作为函数返回值。

NewMainOnly()方法

  1. static ANRError NewMainOnly(long duration) { 
  2.     final Thread mainThread =  //通过getMainEventRunner()方法获取到主线程findThread(EventRunner.getMainEventRunner().getThreadId()); 
  3.     //获取堆栈信息 
  4.     final StackTraceElement[] mainStackTrace = mainThread.getStackTrace(); 
  5.     return new ANRError(new $(getThreadTitle(mainThread), mainStackTrace).new _Thread(null), duration);//返回重新构造的ANRError实例 

New()方法

  1. static ANRError New(long duration, String prefix, boolean logThreadsWithoutStackTrace) { 
  2.     final Thread mainThread = //通过getMainEventRunner()方法获取到主线程findThread(EventRunner.getMainEventRunner().getThreadId()); 
  3.     final Map<Thread, StackTraceElement[]> stackTraces = new TreeMap<Thread, StackTraceElement[]>(new Comparator<Thread>() {@Override...});//获取堆栈信息 
  4.     ... 
  5.     for (Map.Entry<Thread, StackTraceElement[]> entry : stackTraces.entrySet()) 
  6.         tst = new $(getThreadTitle(entry.getKey()), entry.getValue()).new _Thread(tst);//重新构造ANRError实例 
  7.     return new ANRError(tst, duration);//返回重新构造的ANRError实例 

想了解更多内容,请访问:

51CTO和华为官方合作共建的鸿蒙技术社区

https://harmonyos.51cto.com

 

责任编辑:jianghua 来源: 鸿蒙社区
相关推荐

2021-07-06 18:21:31

鸿蒙HarmonyOS应用

2021-04-20 15:06:42

鸿蒙HarmonyOS应用

2021-04-08 14:57:52

鸿蒙HarmonyOS应用

2021-08-30 17:55:58

鸿蒙HarmonyOS应用

2021-11-17 15:37:43

鸿蒙HarmonyOS应用

2021-04-15 17:47:38

鸿蒙HarmonyOS应用

2021-07-20 15:20:40

鸿蒙HarmonyOS应用

2021-11-02 14:54:21

鸿蒙HarmonyOS应用

2021-10-19 10:04:51

鸿蒙HarmonyOS应用

2021-06-29 09:28:16

鸿蒙HarmonyOS应用

2021-03-10 15:03:40

鸿蒙HarmonyOS应用

2021-04-29 14:32:24

鸿蒙HarmonyOS应用

2021-03-24 09:30:49

鸿蒙HarmonyOS应用

2021-06-17 14:56:00

鸿蒙HarmonyOS应用

2021-07-28 09:40:04

鸿蒙HarmonyOS应用

2021-03-03 09:42:26

鸿蒙HarmonyOS图片裁剪

2021-08-26 16:07:46

鸿蒙HarmonyOS应用

2021-08-03 10:07:41

鸿蒙HarmonyOS应用

2021-03-01 14:00:11

鸿蒙HarmonyOS应用

2021-08-05 15:06:30

鸿蒙HarmonyOS应用
点赞
收藏

51CTO技术栈公众号