基于安卓平台的消息弹框组件 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-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:可以模拟主线程死锁。
其模式设置和执行效果如图 2 所示:

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

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

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


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

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

Sample 解析
步骤 1:导入相关类并实例化类对象。
步骤 2:设置 ANRListener 监听。
步骤 3:模拟主线程休眠、无限循环和死锁。
步骤 4:创建 xml 文件。
步骤 5:设置整体布局,并实例化 MyApplication 对象。
步骤 6:设置 ANR 检测模式 Button 的点击事件。
步骤 7:设置 ANR 模拟 Button 的点击事件。
其中,ANRWatchDog 类的作用是检测 ANR 的情况是否出现,ANRError 类的作用是抛出错误信息,即正在运行线程的堆栈追踪信息。
//导入ANRError类和ANRWatchDog类
import com.github.anrwatchdog.ANRError;
import com.github.anrwatchdog.ANRWatchDog;
//实例化ANRWatchDog类对象
ANRWatchDog anrWatchDog = new ANRWatchDog(2000);//设置阻塞响应时间为2000毫秒(2秒)
当需要提前或推迟报告 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异常
}
我们定义:线程阻塞后程序不闪退,而是打印 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);
}
};
线程休眠:
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"),"主线程也申请锁");
在 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>
通过 setUIContent() 方法加载上一步设置好的 xml 文件作为整体显示布局,实例化 MyApplication 对象为后续设置各按钮的点击事件做准备。
setUIContent(ResourceTable.Layout_ability_main);//加载UI布局
final MyApplication application = (MyApplication) getAbilityPackage();//实例化
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");
}
});
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 ;
}
}
});
反之,则说明开发者自定义了监测到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);
}
}
最后需要设置线程休眠、线程无限循环和线程死锁按钮的点击事件。此处以线程休眠按钮为例,只需在对应的 onClick() 方法中调用各自的模拟函数即可,其他两种情况同理。
findComponentById(ResourceTable.Id_threadSleep).setClickedListener(new Component.ClickedListener() {//线程休眠Button的click点击事件
@Override
public void onClick(Component component) {//重写onClick()方法
Sleep();//调用模拟线程休眠的函数
}
});
Library 解析
ANRWatchDog 类提供了两个构造方法,使用第二个带参的构造方法,开发者能够对阻塞响应时间进行设置,使用第一个不带参的构造方法,阻塞响应时间默认配置为 5000ms。
//构造方法一
public ANRWatchDog() {
this(DEFAULT_ANR_TIMEOUT);//使用默认的阻塞响应时间5000ms
}
//构造方法二
public ANRWatchDog(int timeoutInterval) {
super();
_timeoutInterval = timeoutInterval;//自定义阻塞响应时间timeoutInterval
}
任务单元 _ticker 判断主线程是否阻塞:在 ANRWatchDog 类中,监测主线程是否阻塞的具体原理流程如图 9。

在未执行在 _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;
}
};
一种是 _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 的值可以判断 _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); //响应行为设置
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实例
}
👇点击关注鸿蒙技术社区👇
了解鸿蒙一手资讯

点“阅读原文”了解更多




