一、前言:
面对如何分析ANR这个问题,一般都是通过查看/data/anr/ 下的log,分析主线程堆栈、cpu、锁信息等。但是呢,很多时候是没有堆栈信息给你分析的,例如有些高版本设备需要root权限才能访问/data/anr/ 目录,或者来自线上用户反馈,只有一些截图录屏,或者一句话描述。
对于这种线上的ANR就束手无策了,所以要有合适的监控方案,要知道导致ANR的原因,比如卡顿,死锁等
二、ANR简介
Application Not Responding,即应用无响应。
出现的原因:
- KeyDispatchTimeout(5 seconds)按键或触摸事件在特定时间内无响应
- BroadcastTimeout(10 seconds)BoradcastReceiver 在特定的时间内无法处理
- ServiceTimeout(20 seconds)小概率类型 Service 在特定的时间内无法处理完成
三、卡顿分析
每一帧的执行都超过 16.6ms(60fps 的情况下),那么就可能会出现掉帧,也就是卡顿
如果界面线程被阻塞超过一定的时间,就会出现ANR
想要监控应用界面是否发生卡顿,需要先了解一下Android应用主线程的渲染机制
1.Handler处理流程
在应用进程启动的时候,Zygote通过反射调用ActivityThread的main方法,在main方法中调用Looper.loop()来启动looper循环
ActivityThread#main
public static void main(String[] args) {
……
Looper.prepareMainLooper();
……
Looper.loop();
}
Looper.loop()中的for循环
Looper#loop
final Looper me = myLooper();
……
for (;;) {
……
//从MessageQueue中去取消息
Message msg = me.mQueue.next(); // might block
final Printer logging = me.mLogging;
……
//消息处理前
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " "
+ msg.callback + ": " + msg.what);
}
……
//交由handler去分发处理消息
msg.target.dispatchMessage(msg);
……
//消息处理后
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
}
在这个for循环中,通过MessageQueue调用next方法去取消息
MessageQueue#next
Message next() {
for (;;) {
//nextPollTimeoutMillis 不为0 阻塞
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
// 2、先判断当前第一条消息是不是同步屏障消息,
if (msg != null && msg.target == null) {
//3、遇到同步屏障消息,就跳过去取后面的异步消息来处理,同步消息相当于被设立了屏障
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
//4、正常的消息处理,判断是否有延时
if (msg != null) {
if (now < msg.when) {
//3.1
// Next message is not ready. Set a timeout to wake up when it is ready.
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
} else {
// Got a message.
mBlocked = false;
if (prevMsg != null) {
prevMsg.next = msg.next;
} else {
mMessages = msg.next;
}
msg.next = null;
if (DEBUG) Log.v(TAG, "Returning message: " + msg);
msg.markInUse();
return msg;
}
} else {
//5、如果没有取到异步消息,那么下次循环就走到1那里去了,nativePollOnce为-1,会一直阻塞
// No more messages.
nextPollTimeoutMillis = -1;
}
}
}
MessageQueue#next流程解析:
- MessageQueue是一个链表数据结构,判断MessageQueue的头部是不是一个同步屏障消息
所谓同步屏障消息,就是给同步消息加一层屏障,让同步消息不被处理,只会处理异步消息 - 如果是同步屏障消息,也就是target为null,就会跳过MessageQueue中的同步消息,只获取里面的异步消息来处理。如果里面没有异步消息,就会将nextPollTimeoutMillis设置为-1,然后调用nativePollOnce(ptr, nextPollTimeoutMillis); 进入阻塞
- 在正常的消息处理中,不管是异步还是同步消息,先判断是否有带延时,如果是,nextPollTimeoutMillis就会被赋值,下次循环调用nativePollOnce就会阻塞一段时间,如果不是delay消息,就直接返回这个msg,给handler处理;
总结:
next方法是不断从MessageQueue里取出消息,有消息就处理,没有消息就调用nativePollOnce阻塞
nativePollOnce
底层是Linux的epoll机制
2.同步屏障消息
在MessageQueue#next流程中可以看到同步屏障消息跟其他消息相比是没有target,也就是handler的
Android是禁止App往MessageQueue插入同步屏障消息的,我们在调用同步屏障消息时会报错,加了hide的
Looper.getMainLooper().getQueue().postSyncBarrier();
只有系统内部才会使用到同步屏障消息,比如View的绘制流程。
3.View的绘制流程
我们的手机屏幕刷新频率有不同的类型,60Hz、120Hz等。60Hz表示屏幕在一秒内刷新60次,也就是每隔16.6ms刷新一次。屏幕会在每次刷新的时候发出一个 VSYNC
信号,通知CPU进行绘制计算。
view绘制的起点是在 viewRootImpl.requestLayout()
方法开始,这个方法会去执行View三大绘制任务测量布局绘制。
调用requestLayout()
方法之后,并不会马上开始进行绘制任务,而是会给主线程设置一个同步屏障,并设置VSYNC信号监听,当VSYNC信号的到来,会发送一个异步消息到主线程Handler,执行设置的绘制监听任务,并移除同步屏障
好处:保证在ASYNC信号到来之时,绘制任务可以被及时执行,不会造成界面卡顿
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//发送同步屏障
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//设置ASYNC信号的callback
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//ASYNC信号回来之后 移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
}
...
}
Choreographer$FrameDisplayEventReceiver
就是用来接收vsync信号回调的
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
...
@Override
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
...
//
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
//1、发送异步消息
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
// 2、doFrame优先执行
doFrame(mTimestampNanos, mFrame);
}
}
收到Vsync信号回调,会往主线程MessageQueue
post一个异步消息,保证doFrame
优先执行。
然后真正开始绘制View,会调用ViewRootImpl
的doTraversal
、performTraversals
,紧接着会调用View的三大流程measure、layout、draw。
3.异步消息
MessageQueue中的Message的变量isAsynchronous
,标志了这个Message是否是异步消息;标记为true称为异步消息,标记为false称为同步消息。另一个变量target
,标志了这个Message最终由哪个Handler处理
正常使用handler,将Message在插入到MessageQueue中的时候,也就是enqueueMessage时,会强制其target
属性不能为null
boolean enqueueMessage(Message msg, long when) {
// Hanlder不允许为空
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
……
}
postSyncBarrier
方法被标记为@hide
,也就是我们无法调用这个方法
但是还是可以发送异步消息的。在系统添加同步屏障的时候,用以下方法可以将消息设置成异步消息,
- 使用异步类型的Handler发送的全部Message都是异步的
通过Handler有一个需要传递boolean async的构造函数,赋值给mAsynchronous,在发送消息的时候就会给Message赋值,但是带异步的构造函数是hide的是无法直接调用的,低版本可以使用反射的方式去创建,api28之后添加了createAsync,可以直接创建异步的Handler,异步Handler发出来的消息则全是异步的。 - 给Message标志异步
通过Message.setAsynchronous
方法
App要谨慎使用异步消息
4.dispatchMessage
以上分析,都不可能造成卡顿,只剩下Handler#dispatchMessage了
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
调用逻辑:
- Handler#post(Runnable r):将这个r封装到Message的callback中
- 构造方法传CallBack,
public Handler(@Nullable Callback callback, boolean async) {}
- Handler重写handleMessage方法
5.卡顿总结
应用卡顿,原因一般都是Handler处理消息太耗时导致的。可能是方法是本身太耗时、算法效率低、cpu被抢占、内存不足、IPC超时
四、卡顿排查工具
1.CPU Profiler
Android studio自带的工具,可以查看 CPU、内存、网络和电池资源的使用情况
以图形的形式展示执行时间、调用栈,信息全面包含所有线程
使用方式:
Debug.startMethodTracing("xxx");
...
Debug.stopMethodTracing();
可以通过它来检测区间的代码,最终会在 sd 卡的 Android/data/packagename/files 文件夹下生成一个 xxx.trace 文件
可以通过AS来分析出这段时间内函数的调用以及耗时,帧率,CPU 以及其他线程的情况
缺点:
工具本身会带来很大的性能开销,可能无法反映真实的情况
2.Systrace
五、卡顿监控
1.方案一:
在Looper#loop方法中,在dispatchMessage前后,提供了logging.println这个接口,调用Looper.getMainLooper().setMessageLogging(printer)
,即可从回调中拿到Handler处理一个消息的前后时间,来监听Handler处理消息耗时
监听到发生卡顿之后,dispatchMessage
早已调用结束,已经出栈,此时再去获取主线程堆栈,堆栈中是不包含卡顿的代码的
所以需要在后台开一个线程,定时获取主线程堆栈,将时间点作为key,堆栈信息作为value,保存到Map中,在发生卡顿的时候,取出卡顿时间段内的堆栈信息即可
注意:
这种方式只适合线下,因为会打印大量日志,存在字符串拼接,频繁调用,会创建大量对象,造成内存抖动。后台线程频繁获取主线程堆栈,对性能有一定影响,获取主线程堆栈,会暂停主线程的运行
2.方案二:
字节码插桩
在编译期或运行期修改Java或Kotlin源代码编译生成的字节码的过程。通过字节码插桩,我们可以在不改变源代码的情况下,对应用程序的行为进行定制和增强
- 避免方法数暴增:在方法的入口和出口应该插入相同的函数,在编译时提前给代码中每个方法分配一个独立的 ID 作为参数。
- 过滤简单的函数:过滤一些类似直接 return、i++ 这样的简单函数,并且支持黑名单配置。对一些调用非常频繁的函数,需要添加到黑名单中来降低整个方案对性能的损耗。
- 优化编译期的耗时。可以通过线程池以异步的方式插桩,结合 Future。
- 运行时存储方法信息的方式。将方法信息以 int 值保存,类似于 MeasureSpec 的设计
微信的开源项目 matrix
中 Trace Canary 模块已经解决上述问题
六、ANR分析
ANR发生时,会把发生ANR时的线程堆栈、cpu等信息保存起来
一般都是分析 /data/anr/traces.txt 文件
死锁导致ANR分析:
首先看主线程,搜索 main,
ANR日志中有很多信息,可以看到,主线程id是1(tid=1),在等待一个锁,这个锁一直被id为t0的程持有
- waiting to lock <xxxxxxx>(a java.lang.0bject) held by thread t0
locked <yyyyyyyy> (a java.lang.0bject)
t0线程是Blocked状态,正在等待一个锁,这个锁被id为1的线程持有,同时这个t0线程还持有一个锁,这个锁是主线程想要的。
- waiting to lock <yyyyyyyy>(a java,lang.0bject) held by thread 1
locked <xxxxxxx>(a iava.lang.0biect)
通过ANR日志,可以很清楚分析出这个ANR是死锁导致的,并且有具体堆栈信息。
实际项目中,还有很多情况会导致ANR,例如内存不足、CPU被抢占、系统服务没有及时响应
七、ANR 监控
常规的线下分析方法有点繁琐,需要pull出anr日志,然后分析线程堆栈等信息,并且不能带到线上
如何搭建一个完善的ANR监控系统?
1.抓取系统traces.txt 上传
步骤:
- 当监控线程发现主线程卡死时,主动向系统发送SIGNAL_QUIT信号
- 等待/data/anr/traces.txt文件生成
- 文件生成以后进行上报
问题:
- traces.txt 里面包含所有线程的信息,上传之后需要人工过滤分析
- 很多高版本系统需要root权限才能读取 /data/anr这个目录