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

朱伟ISRC
发布于 2021-7-30 15:45
浏览
19收藏

 前言 

          基于安卓平台的消息弹框组件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-鸿蒙开发者社区图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-鸿蒙开发者社区 图2 线程休眠设置流程和执行效果

鸿蒙开源第三方组件——ANR异常监测组件 ANR-WatchDog-ohos-鸿蒙开发者社区图3 监测线程休眠后闪退输出的HiLog信息

2、线程无限循环

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

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

鸿蒙开源第三方组件——ANR异常监测组件 ANR-WatchDog-ohos-鸿蒙开发者社区图4 线程无限循环设置流程和执行效果

鸿蒙开源第三方组件——ANR异常监测组件 ANR-WatchDog-ohos-鸿蒙开发者社区 图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-鸿蒙开发者社区 图6 线程死锁设置流程和执行效果

鸿蒙开源第三方组件——ANR异常监测组件 ANR-WatchDog-ohos-鸿蒙开发者社区图7 监测线程死锁后闪退输出的HiLog信息

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

鸿蒙开源第三方组件——ANR异常监测组件 ANR-WatchDog-ohos-鸿蒙开发者社区 图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判断主线程是否阻塞

鸿蒙开源第三方组件——ANR异常监测组件 ANR-WatchDog-ohos-鸿蒙开发者社区 图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实例
}

项目贡献人

           陈丛笑 郑森文 朱伟 陈美汝 李珂

©著作权归作者所有,如需转载,请注明出处,否则将追究法律责任
已于2021-7-30 15:57:43修改
32
收藏 19
回复
举报
25条回复
按时间正序
/
按时间倒序
鱼儿会飞啦啦啦
鱼儿会飞啦啦啦

三连支持小姐姐啦

2
回复
2021-7-30 16:19:10
朱伟ISRC
朱伟ISRC 回复了 鱼儿会飞啦啦啦
三连支持小姐姐啦

爱你~

回复
2021-7-30 16:20:54
大白兔的耳朵
大白兔的耳朵

支持支持,

1
回复
2021-7-30 16:25:57
Junfeng0613
Junfeng0613

又是个很实用的组件啊

回复
2021-7-30 16:43:36
朱伟ISRC
朱伟ISRC 回复了 Junfeng0613
又是个很实用的组件啊

谢谢支持!

回复
2021-7-30 16:50:36
朱伟ISRC
朱伟ISRC 回复了 大白兔的耳朵
支持支持,

谢谢!

回复
2021-7-30 16:50:55
上海至金黄色
上海至金黄色

这个组件科技含量高哦

回复
2021-7-30 17:46:01
上海至金黄色
上海至金黄色

支持一下,现在踏实做组件库的人不多了

回复
2021-7-30 17:46:26
咕咕咕咕咕咕
咕咕咕咕咕咕

写的真不错啊!

回复
2021-8-2 10:40:16
朱伟ISRC
朱伟ISRC 回复了 上海至金黄色
这个组件科技含量高哦

谢谢支持

回复
2021-8-3 17:25:09
朱伟ISRC
朱伟ISRC 回复了 咕咕咕咕咕咕
写的真不错啊!

谢谢支持!

回复
2021-8-3 17:25:36
拓维云创_LT
拓维云创_LT

写的很不错,一个实用的组件

2
回复
2021-8-18 15:40:38
朱伟ISRC
朱伟ISRC 回复了 拓维云创_LT
写的很不错,一个实用的组件

谢谢支持!

回复
2021-8-18 18:09:55
小道安全
小道安全

好文,anr详解

回复
2021-8-19 11:39:24
朱伟ISRC
朱伟ISRC 回复了 小道安全
好文,anr详解

谢谢支持!

回复
2021-8-19 14:15:15
小道安全
小道安全

好文,刚好之前项目碰到,学习下新思路

1
回复
2021-8-24 11:26:12
朱伟ISRC
朱伟ISRC 回复了 小道安全
好文,刚好之前项目碰到,学习下新思路

能帮到你很开心!

1
回复
2021-8-24 12:03:56
mb6126ee97731d9
mb6126ee97731d9

左手写编程RCS1236右手绘生活

2
回复
2021-8-26 09:34:53
朱伟ISRC
朱伟ISRC 回复了 mb6126ee97731d9
左手写编程RCS1236右手绘生活

虽然听不懂,但是你说得对!

回复
2021-8-26 10:29:36
wx6135cf9ed44b0
wx6135cf9ed44b0

写的太棒了

1
回复
2021-9-6 16:24:13
回复
    相关推荐