一、前言
最近参加了几轮面试,发现很多5-7年工作经验的候选人在性能优化这一块,基本上只能说出传统的分析方式,例如ANR分析,是通过查看/data/anr/ 下的log,分析主线程堆栈、cpu、锁信息等,
然而,这种方法有一定的局限性,并不是每次都奏效,很多时候是没有堆栈信息给你分析的,例如有些高版本设备需要root权限才能访问/data/anr/ 目录,或者是线上用户的反馈,只有一张ANR的截图加上一句话描述。
假如你的App没有实现ANR监控上报,那么你大概率会把这个问题当成“未复现”处理掉,而没有真正解决问题。
于是我整理了这一篇文章,主要关于卡顿、ANR、死锁监控方案。
二、卡顿原理和监控
2.1 卡顿原理
一般来说,主线程有耗时操作会导致卡顿,卡顿超过阈值,触发ANR。
从源码层面一步步分析卡顿原理:
首先应用进程启动的时候,Zygote
会反射调用 ActivityThread
的 main 方法,启动 loop 循环
->ActivityThread
public static void main(String[] args) {
...
Looper.prepareMainLooper();
Looper.loop();
...
}
看下Looper的loop方法
->Looper
public static void loop() {
for (;;) {
//1、取消息
Message msg = queue.next(); // might block
...
//2、消息处理前回调
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
...
//3、消息开始处理
msg.target.dispatchMessage(msg);// 分发处理消息
...
//4、消息处理完回调
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
}
...
}
由于loop循环存在,所以主线程可以长时间运行。如果想要在主线程执行某个任务,唯一的办法就是通过主线程Handler post一个任务到消息队列里去,然后loop循环中拿到这个msg,交给这个msg的target处理,这个target是Handler。
从上面的代码块可以看出,导致卡顿的原因可能有两个地方
- 注释1的
queue.next()
阻塞, - 注释3的
dispatchMessage
耗时太久。
2.1.1 MessageQueue#next 耗时
看下源码
MessageQueue#next
Message next() {
for (;;) {
//1、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;
}
}
next
方法的大致流程是这样的:
-
MessageQueue是一个链表数据结构,判断MessageQueue的头部(第一个消息)是不是一个同步屏障消息,所谓同步屏障消息,就是给同步消息加一层屏障,让同步消息不被处理,只会处理异步消息;
-
如果遇到同步屏障消息,就会跳过MessageQueue中的同步消息,只获取里面的异步消息来处理。如果里面没有异步消息,那就会走到注释5,nextPollTimeoutMillis设置为-1,下次循环调用注释1的nativePollOnce就会阻塞;
-
如果looper能正常获取到消息,不管是异步消息或者同步消息,处理流程都是一样的,在注释4,先判断是否带延时,如果是,nextPollTimeoutMillis就会被赋值,然后下次循环调用注释1的nativePollOnce就会阻塞一段时间。如果不是delay消息,就直接返回这个msg,给handler处理;
从上面分析可以看出,next
方法是不断从MessageQueue里取出消息,有消息就处理,没有消息就调用nativePollOnce
阻塞,nativePollOnce
底层是Linux的epoll机制,这里涉及到一个Linux IO 多路复用的知识点
Linux IO 多路复用,select、poll、epoll
Linux 上IO多路复用方案有 select、poll、epoll。它们三个中 epoll 的性能表现是最优秀的,能支持的并发量也最大。
- select 是操作系统提供的系统调用函数,通过它,我们可以把一个文件描述符的数组发给操作系统, 让操作系统去遍历,确定哪个文件描述符可以读写, 然后告诉我们去处理。
- poll:它和 select 的主要区别就是,去掉了 select 只能监听 1024 个文件描述符的限制
- epoll:epoll 主要就是针对select的这三个可优化点进行了改进
1、内核中保存一份文件描述符集合,无需用户每次都重新传入,只需告诉内核修改的部分即可。 2、内核不再通过轮询的方式找到就绪的文件描述符,而是通过异步 IO 事件唤醒。 3、内核仅会将有 IO 事件的文件描述符返回给用户,用户也无需遍历整个文件描述符集合。
关于epoll机制就总结这么多啦,可以参考 图解 | 深入揭秘 epoll 是如何实现 IO 多路复用的
回到 MessageQueue
的next
方法,看看哪里可能阻塞
同步屏障消息没移除导致next一直阻塞
有一种情况,在存在同步屏障消息的情况下,当异步消息被处理完之后,如果没有及时把同步屏障消息移除,会导致同步消息一直没有机会处理,一直阻塞在nativePollOnce。
同步屏障消息
Android 是禁止App往MessageQueue插入同步屏障消息的,代码会报错
系统一些高优先级的操作会使用到同步屏障消息,例如View在绘制的时候,最终都要调用ViewRootImpl
的scheduleTraversals
方法,会往MessageQueue
插入同步屏障消息,绘制完成后会移除同步屏障消息。
->ViewRootImpl
@UnsupportedAppUsage
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
//插入同步屏障消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
void unscheduleTraversals() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步屏障消息
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
mChoreographer.removeCallbacks(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}
为了保证View的绘制过程不被主线程其它任务影响,View在绘制之前会先往MessageQueue插入同步屏障消息,然后再注册Vsync信号监听,Choreographer$FrameDisplayEventReceiver</