Android-ANR优化之基于消息队列的卡顿捕获功能

起因

掐指一算,又有几个月没有写博客了,最近工作任务一直放在解决ANR的问题上,有些ANR很好解决,可是bugly后台统计的ANR大多都是长这样:

android.os.MessageQueue.next(MessageQueue.java:355)
android.os.Looper.loop(Looper.java:144)
android.app.ActivityThread.main(ActivityThread.java:6861)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:450)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)

还有这样:

android.os.MessageQueue.nativePollOnce(Native Method)
android.os.MessageQueue.next(MessageQueue.java:324)
android.os.Looper.loop(Looper.java:136)
android.app.ActivityThread.main(ActivityThread.java:6244)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:891)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:781)

这样的ANR长期占据了top20,必须得想办法解决,相信你们也碰到不少,看过Handler相关源码的都知道这个是和Handler消息队列相关,没看过的可以看我这篇 Android面试题-Handler机制,这几个方法明明不耗时啊,怎么会产生ANR呢?并且这些都是线上的ANR,我们也拿不到trace日志,这咋解决呢?

后来通过查阅资料,找到了今日头条关于ANR的分析文章,其中就有提到相关消息队列的ANR问题,这不正是我所碰到的ANR问题吗?我看完后收获颇多,强烈推荐大家看看。今日头条 ANR 优化实践系列

导致ANR的原因

通过字节的文章,可以看到对于导致ANR的原因归为以下几类:

  1. 当前 Trace 堆栈所在业务耗时严重;
  2. 当前 Trace 堆栈所在业务耗时并不严重,但是历史调度有一个严重耗时;
  3. 当前 Trace 堆栈所在业务耗时并不严重,但是历史调度有多个消息耗时;
  4. 当前 Trace 堆栈所在业务耗时并不严重,但是历史调度存在巨量重复消息(业务频繁发送消息);
  5. 当前 Trace 堆栈业务逻辑并不耗时,但是其他线程存在严重资源抢占,如 IO,Mem,CPU;
  6. 当前 Trace 堆栈业务逻辑并不耗时,但是其他进程存在严重资源抢占,如 IO,Mem,CPU;

之所以会出现 MessageQueue.nativePollOnce 和 MessageQueue.next 是由于这两个方法在主线程中是高频执行的,由于前面的消息产生了耗时,不断堆积,等发生了ANR正好执行在这两个方法上,于是它们成了替罪羊被系统捕获。那么我们该怎样去获取到当前发生卡顿的消息呢?
消息卡顿示意图

如何判断当前消息发生卡顿

既然要获取当前发生卡顿的消息,那么首先我们就需要判断当前消息发生了卡顿,那么如何判断呢?字节自己实现了一套 Raster 监控工具,由于目前没有开源(说是马上会开源,不介意的就再等等吧),我们既然没有他们的技术实力,只有自己想办法。

既然是消息队列发生了卡顿,那我们是不是可以监控消息队列?通过一个监控线程,每隔1秒向主线程消息队列的头部插入一条空消息。假设1秒后这个消息并没有被主线程消费掉,说明阻塞消息运行的时间在0~1秒之间。换句话说,如果我们需要监控3秒卡顿,那在第4次轮询中头部消息依然没有被消费的话,就可以确定主线程出现了一次3秒以上的卡顿。

看着貌似可以哈?来实现代码看看,于是它来了!!代码如下,代码很简单,我就不注释了:

基于消息队列监控卡顿的Runnable

import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;

/**
 * author : fySpring
 * time   : 2021/08/21
 * desc   :
 */
public class AnrCheckerRunnable implements Runnable {
    public static final String TAG = "AnrCheckerRunnable";
    private final Handler mHandler = new Handler(Looper.getMainLooper());
    private int count = 0;
    private boolean isShowLog = false;
    private final StringBuilder anrSb = new StringBuilder();
    private static final int ANR_TAG = 100;

    @Override
    public void run() {
        try {
            while (true) {
                if (mHandler.hasMessages(ANR_TAG)) {
                    count++;
                    if (!isShowLog && count > 2) {
                        StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
                        for (StackTraceElement s : stackTrace) {
                            anrSb.append(s.toString() + "\n");
                        }
                        isShowLog = true;
                    }
                } else {
                    if (count > 0 && anrSb.length() > 0) {
                        //TODO upload anr info
                        Log.e(TAG, "发生一次耗时" + count + "s的卡顿\n" + anrSb.toString());
                        anrSb.delete(0, anrSb.length());
                    }
                    Log.i(TAG, "正常执行一次空消息  start");
                    count = 0;
                    isShowLog = false;
                    Message msg = mHandler.obtainMessage();
                    msg.what = ANR_TAG;
                    mHandler.sendMessageAtFrontOfQueue(msg);
                }
                Thread.sleep(1000L);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

写在最后

该方案不能保证绝对准确,我在测试过程中也碰到一些毫无关联的堆栈,这个只是一个帮助你分析的简易的手段,在这里我推荐腾讯的Matrix ,通过插桩的方式来获取当前卡顿堆栈,比这种通过队列的形式准确的多,但是功能全部导进来项目大了5.5MB左右,根据你们的需求来定吧~

ANR优化这条路太难了!!我目前的资历尚浅,还得多学习!好了,我又要去学习了~
在这里插入图片描述

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值