起因
掐指一算,又有几个月没有写博客了,最近工作任务一直放在解决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的原因归为以下几类:
- 当前 Trace 堆栈所在业务耗时严重;
- 当前 Trace 堆栈所在业务耗时并不严重,但是历史调度有一个严重耗时;
- 当前 Trace 堆栈所在业务耗时并不严重,但是历史调度有多个消息耗时;
- 当前 Trace 堆栈所在业务耗时并不严重,但是历史调度存在巨量重复消息(业务频繁发送消息);
- 当前 Trace 堆栈业务逻辑并不耗时,但是其他线程存在严重资源抢占,如 IO,Mem,CPU;
- 当前 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优化这条路太难了!!我目前的资历尚浅,还得多学习!好了,我又要去学习了~