暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

一个非常实用的鸿蒙监测组件!

鸿蒙技术社区 2021-08-26
202

基于安卓平台的消息弹框组件 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 中,蓝色框内是 ANR 的监测模式设置按钮,红色框内的是 ANR 模拟按钮。

图 1:ANR-WatchDog-ohos 组件应用的界面介绍


下面具体解释各按钮的含义:


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 发生时组件的运行效果。

为了更清楚的展现检测模式的作用,我们给每个 ANR 模拟按钮设置不同的检测模式。


下面对组件的运行效果进行详细描述:


(1)线程休眠


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


点击 Thread Sleep 按钮,启动主线程休眠后,ANR-WatchDog-ohos 组件监测到程序在 2 秒内一直无响应,于是触发应用闪退,并通过 HiLog 报告所有线程的 ANR 详情。


其模式设置和执行效果如图 2 所示:

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


在报告中,可以根据“Caused by”后面的堆栈信息追踪查看线程休眠的具体原因,如图 3 所示。

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


(2)线程无限循环


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


点击 Infinite loop 按钮,启动线程无限循环后,ANR-WatchDog-ohos 组件监测到程序在 4 秒内一直无响应,于是触发应用闪退,并通过 HiLog 报告主线程的 ANR 错误详情。


其监测模式设置和执行效果如图 4 所示:

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


HiLog 报告主线程的 ANR 详情如图 5 所示:


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

(3)线程死锁


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


点击 Dead lock 按钮,启动线程死锁后,ANR-WatchDog-ohos 组件监测到程序在 6 秒内一直无响应,于是触发应用闪退,并通过 HiLog 报告以“APP:”为前缀线程的 ANR 错误详情。


其监测模式设置和执行效果如图 6 所示:
图 6:线程死锁设置流程和执行效果


HiLog 报告主线程的 ANR 详情如图 7 所示:

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


值得注意的是:无论在哪种 ANR 类型下,只要将 Behaviour 设置为 Silent,应用遇到 ANR 时的响应行为都需要开发者自定义。


例如此处我们定义:应用遇到 ANR 的情况时,通过 HiLog 打印出 ANR-Watchdog-Demo 的 tag,如图 8 所示:

图 8:Silent 行为下不闪退只输出 HiLog 信息

Sample 解析


ANR-WatchDog-ohos 组件能够监测多种类型的 ANR 错误,及时捕捉并触发相应的响应行为。


下面将具体讲解 ANR-WatchDog-ohos 组件的使用方法,共分为 7 个步骤:
  • 步骤 1:导入相关类并实例化类对象。

  • 步骤 2:设置 ANRListener 监听。

  • 步骤 3:模拟主线程休眠、无限循环和死锁。

  • 步骤 4:创建 xml 文件。

  • 步骤 5:设置整体布局,并实例化 MyApplication 对象。

  • 步骤 6:设置 ANR 检测模式 Button 的点击事件。

  • 步骤 7:设置 ANR 模拟 Button 的点击事件。









其中步骤 1 至步骤 2 在 MyApplication 文件中进行,步骤 3 至步骤 7 在MainAbility文件中进行。

①导入相关类并实例化类对象


在 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秒)


②设置 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);
    }
};


③模拟线程休眠、线程无限循环和线程死锁


为了使 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"),"主线程也申请锁");


④创建 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>


⑤设置整体布局,并实例化 MyApplication 对象

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

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


⑥设置 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);
        }
    }


⑦设置 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 类的内部逻辑。

①ANRWatchDog 类


构造方法阻塞响应时间:ANRWatchDog 类继承自 Thread 类,其实质是一个线程,因此根据线程的特性,我们可以随时将其中断。


ANRWatchDog 类提供了两个构造方法,使用第二个带参的构造方法,开发者能够对阻塞响应时间进行设置,使用第一个不带参的构造方法,阻塞响应时间默认配置为 5000ms。

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


任务单元 _ticker 判断主线程是否阻塞:在 ANRWatchDog 类中,监测主线程是否阻塞的具体原理流程如图 9。

图 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);  //响应行为设置


②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实例
}


项目贡献人:陈丛笑、郑森文朱伟陈美汝李珂


👇点击关注鸿蒙技术社区👇

了解鸿蒙一手资讯


“阅读原文”了解更多

文章转载自鸿蒙技术社区,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论