前言
基于安卓平台的消息弹框组件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:可以模拟主线程死锁。
图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所示。
图2 线程休眠设置流程和执行效果
图3 监测线程休眠后闪退输出的HiLog信息
2、线程无限循环
ANR监测模式:阻塞响应时间为4秒,报告模式为All Threads、响应行为Crash。
点击Infinite loop按钮,启动线程无限循环后,ANR-WatchDog-ohos组件监测到程序在4秒内一直无响应,于是触发应用闪退,并通过HiLog报告主线程的ANR错误详情,其监测模式设置和执行效果如图4所示,HiLog报告主线程的ANR详情如图5所示。
图4 线程无限循环设置流程和执行效果
图5 监测线程无限循环后闪退输出的HiLog信息
3、线程死锁
ANR监测模式:阻塞响应时间为6秒,报告模式为Filtered(只报告以“APP:”为前缀的线程)、响应行为Crash。
点击Dead lock按钮,启动线程死锁后,ANR-WatchDog-ohos组件监测到程序在6秒内一直无响应,于是触发应用闪退,并通过HiLog报告以“APP:”为前缀线程的ANR错误详情,其监测模式设置和执行效果如图6所示,HiLog报告主线程的ANR详情如图7所示。
图6 线程死锁设置流程和执行效果
图7 监测线程死锁后闪退输出的HiLog信息
值得注意的是:无论在哪种ANR类型下,只要将Behaviour设置为Silent,应用遇到ANR时的响应行为都需要开发者自定义。例如此处我们定义:应用遇到ANR的情况时,通过HiLog打印出ANR-Watchdog-Demo的tag,如图7所示:
图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判断主线程是否阻塞
图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实例
- }