android:App卡顿监控的技术原理

目前业界主流的几种有效的监控方式如下:

  • 子线程不断轮询主线程。

  • 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() {    @Override    public void doFrame(long frameTimeNanos) {        // 这里可以统计相邻间隔,判断卡顿,也可以统计doFrame 帧率        Choreographer.getInstance().postFrameCallback(this);    }});

这种方式优点:使用简单,不仅支持卡顿监控,还支持计算帧率。缺点就是:需要另开子线程来获取堆栈信息,会消耗部分系统资源。

原创文章,作者:guozi,如若转载,请注明出处:https://www.sudun.com/ask/80813.html

(0)
guozi's avatarguozi
上一篇 2024年5月31日 上午10:27
下一篇 2024年5月31日 上午10:36

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注