目前业界主流的几种有效的监控方式如下:
-
子线程不断轮询主线程。
-
Looper Printer
-
Choreographer FrameCallback
方 式1
我们可以开一个子线程不断去轮询主线程,原理和实现方法也很简单:就是不断向主线程发送Message,每隔一段时间检查一次刚刚发送的消息是否被处理,如果没被处理,说明这段时间主线程被卡住了。
这种方式优点就是:实现简单,能够监控各种类型的卡顿,缺点就是:使用轮询方式,不够优雅,而且轮询时间长短不好确定,时间间隔越短,对性能影响越大,反之,容易漏报。
原因:如我的轮询间隔设了3s,在1.5s~4.5s发生了卡顿,我是监测不到的,因为0~3s 和 3s ~ 6s 都有不卡顿的地方,发送的Message 都能被处理掉, 所以当我设置卡顿阈值为 3s 时, 这个卡顿就被漏报了。没什么特别好办法,只能调整时间阈值与漏报率达到一个平衡。
代码片段:
class UiMonitorThread implements Runnable {
@Override public void run() {
while (isRunning) {
// 每隔 1.5s 往主线程发一次消息
uiMonitorHandler.sendEmptyMessage(id);
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 如果连续两次消息都没被处理掉,则认为发生了卡顿
checkMessageHandled();
}
}
}
方式2
我们可以使用系统方法 setMessageLogging 替换掉主线程 Looper 的 Printer 对象,通过计算 Printer 打印日志的时间差,来拿到系统 dispatchMessage 方法的执行时间。
Looper.getMainLooper().setMessageLogging(str -> {
// 计算相邻两次日志时间间隔
});
这种方式的优点就是:实现简单,不会漏报,缺点就是,一些类型的卡顿无法被监控到。
android.os.Looper#loop() 源码片段:
通过代码可看到,仅监控 dispatchMessage 并不能cover 住所有卡顿,mQueue.next 注释很清楚了,might block。其中包括:nativePollOnce 方法和 idler.queueIdle()方法。其中me.mQueue.next 源码片段:
nativePollOnce 方法很重要,除了主线程空闲时会阻塞在这里,view 的touch事件也都是在这里被处理的。所以如果应用内包含了很多自定义 view,或处理了很多 onTouch 事件,就很难接受了。
不仅这样,Native Message 也会卡在 nativePollOnce 方法内,所以同样无法监控到。
queueIdle() 方法会在主线程空闲的时候被调用,所以如果我们在这里有耗时操作,也有可能引起卡顿的,而这种卡顿同样无法监控。
另一种引起卡顿的场景:就是常说的同步屏障了(第一次听到这个名字一脸懵逼)。我们 Message 默认都是同步消息,当我们调用 invalidate 来刷新UI 时,最终都会调用到 ViewRootImpl中的scheduleTraversals 方法,会向主线程 Looper postSyncBarrier 插入同步屏障消息,目的是刷新 UI 时,让 Looper 中的同步消息都被跳过,使渲染UI的同步屏障消息得到优先处理。
为啥说同步屏障会引起卡顿了,根据代码可看到,scheduleTraversals 方法和 unscheduleTraversals 是配对的,但都不是线程安全的方法。如果在异步线程 invalidate,导致多次执行 scheduleTraversals 方法,而 unscheduleTraversals 又只能移除最后的 mTraversalBarrier,那就会造成主线程的 Looper 的同步消息一直得不到处理,从而引起卡死。
虽然说了这么多问题,但是呢,作为一个主流的监控方案,一些缺陷已经有了解决方案。
- nativePollOnce 的 onTouchEvent监控
我们可以通过ELF Hook, hook 到 libinput.so 的 recvform 和 sendto 方法,用我们自己的方法替换,在这里做监控,当调用 recvform 方法时,说明我们的应用接收到了 onTouch 事件,当被调用 sendto 方法时,说明 onTouch 事件已经被消费。
关于系统的 input 系统,后续文章会介绍。
- IdleHandler#queueIdle 监控
看源码可知,ArrayList mIdleHandlers 保存着全部我们所需的 IdleHandler,那么我们完全可以通过反射赋值成我们自己的MyArrayList,并重写 MyArrayList 的 add 方法,是不是就可以监控到每个被添加的 IdleHandler呢?
在 add 方法内拿到被添加的 IdleHandler 后,我们就可以监控 queueIdle 方法执行的时间了,代码片段:
static class MyArrayList<E> extends ArrayList {
@Override
public boolean add(Object o) {
if (o instanceof MessageQueue.IdleHandler) {
super.add(new MyIdleHandler((MessageQueue.IdleHandler)o));
}
return super.add(o);
}
}
static class MyIdleHandler implements MessageQueue.IdleHandler {
private final MessageQueue.IdleHandler idleHandler;
MyIdleHandler(MessageQueue.IdleHandler idleHandler) {
this.idleHandler = idleHandler;
}
@Override
public boolean queueIdle() {
// 监控 idleHandler.queueIdle() 耗时即可
return this.idleHandler.queueIdle();
}
}
- 同步屏障卡死监控
我们可以定时的通过反射去拿 MessageQueue 的 mMessages,如果发现 mMessages.target=null,并且 mMessages.when 已经很长时间了,就有可能发生同步屏障消息泄漏了,这时我们可以再主动向主线程Looper 发送一个同步消息和一个异步消息,如果同步消息无法执行,但异步消息被处理,这时基本可以确定泄漏了。
我们可以通过反射去 removeSyncBarrier(token),其中token 为 mMessages.arg1。
方式3
Android 从4.1开始加入 Choreographer 用于同 VSync 机制配合,实现统一调度绘制界面。我们可以设置 Choreographer 类的 FrameCallback 函数,当每一帧被渲染时会触发 FrameCallback 回调,FrameCallback 回调 doFrame(long frameTimeNanos) 函数,一次界面渲染会回调 doFrame,如果两次 doFrame 间隔大于16.6ms 则发生了卡顿。而 1s 内有多少次 callback,就代表了实际的帧率。
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
public void doFrame(long frameTimeNanos) {
// 这里可以统计相邻间隔,判断卡顿,也可以统计doFrame 帧率
Choreographer.getInstance().postFrameCallback(this);
}
});
这种方式优点:使用简单,不仅支持卡顿监控,还支持计算帧率。缺点就是:需要另开子线程来获取堆栈信息,会消耗部分系统资源。
原创文章,作者:guozi,如若转载,请注明出处:https://www.sudun.com/ask/80813.html