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

七分青年 2021-08-26 14:50:36 3950

基于安卓平台的消息弹框组件 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"
.../>
... //线程无限循环按钮和死锁按钮同上

⑤设置整体布局,并实例化 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() {@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实例
}

来源:鸿蒙技术社区

声明:本文内容由易百纳平台入驻作者撰写,文章观点仅代表作者本人,不代表易百纳立场。如有内容侵权或者其他问题,请联系本站进行删除。
红包 93 收藏 评论 打赏
评论
0个
内容存在敏感词
手气红包
    易百纳技术社区暂无数据
相关专栏
置顶时间设置
结束时间
删除原因
  • 广告/SPAM
  • 恶意灌水
  • 违规内容
  • 文不对题
  • 重复发帖
打赏作者
易百纳技术社区
七分青年
您的支持将鼓励我继续创作!
打赏金额:
¥1易百纳技术社区
¥5易百纳技术社区
¥10易百纳技术社区
¥50易百纳技术社区
¥100易百纳技术社区
支付方式:
微信支付
支付宝支付
易百纳技术社区微信支付
易百纳技术社区
打赏成功!

感谢您的打赏,如若您也想被打赏,可前往 发表专栏 哦~

举报反馈

举报类型

  • 内容涉黄/赌/毒
  • 内容侵权/抄袭
  • 政治相关
  • 涉嫌广告
  • 侮辱谩骂
  • 其他

详细说明

审核成功

发布时间设置
发布时间:
是否关联周任务-专栏模块

审核失败

失败原因
备注
拼手气红包 红包规则
祝福语
恭喜发财,大吉大利!
红包金额
红包最小金额不能低于5元
红包数量
红包数量范围10~50个
余额支付
当前余额:
可前往问答、专栏板块获取收益 去获取
取 消 确 定

小包子的红包

恭喜发财,大吉大利

已领取20/40,共1.6元 红包规则

    易百纳技术社区