Android 流畅性三板斧之卡顿监控

1,795 阅读6分钟

Android 流畅性三板斧之卡顿监控

Android 流畅性监控的三板斧,帧率的监控、卡顿监控、ANR的监控。之所以讲这三者放在一起是他们的联系比较密切。帧率的下降往往伴随着有卡顿,【过分卡顿】往往就会产生ANR。

严谨的讲,帧率下降不一定会有卡顿(这里对卡顿是从技术角度定义在主线程执行了耗时任务),卡顿产生的原因还有其他因素导致,比如系统负载、CPU繁忙等。

【过分的卡顿】也不一定产生ANR,卡顿但未触发ANR产生的条件就不会产生ANR。关于ANR的详细内容我们放在三板斧系列文章的第三篇。

Android 流畅性三板斧之帧率监控juejin.cn/post/721780…

Android 流畅性三板斧之卡顿监控juejin.cn/post/721877…

Android 流畅性三板斧之ANR监控 (juejin.cn/post/722004…)

温馨提示,本文涉及的实现的代码以上传至github github.com/drummor/Get…,结合代码食用更佳

1 WatchDog 方案

1.1 基本原理

思路:

开启一个子线程不断往主线程Looper 发消息。这个消息内就是修改我们重置一个值。子线程任务间隔一段时间check 消息是否被处理,如果没有被处理,则认为主线程出现了卡顿情况。

代码:

final Runnable _ticker = new Runnable() {
        @Override public void run() {
            _tick = 0;
            _reported = false;
        }
    };

  @Override
    public void run() {
        setName("|ANR-WatchDog|");
        long interval = _timeoutInterval;
        while (!isInterrupted()) {
            boolean needPost = _tick == 0;
            _tick += interval;
            if (needPost) {
                _uiHandler.post(_ticker);
            }
            try {
                Thread.sleep(interval);
            } catch (InterruptedException e) {
                _interruptionListener.onInterrupted(e);
                return ;
            }
            if (_tick != 0 && !_reported) {
                //认为发生了卡顿
            }
        }

1.2 方案存在的问题

1.2.1 问题1

不断的往message 轮训,对性能和功耗有损伤,不够优雅;同时设置的IdelHandler 任务永远得不到执行。

1.2.2 问题2

上面这个问题还好,更严重的问题是漏报。如下图所示。

image.png

  • 500ms检测一次有一个耗时600ms的任务,执行执行的开始是在检测第一个检测周期的第150ms处结束。在第一个周期的0-50m和第二个周期的150-500ms都有一段不卡顿的时间,这段时间检测消息能够被处理掉。那么该耗时任务能被检测到的概率会只有有20%。

    只有当卡顿时间大于两倍的轮训间隔,卡顿才能完整的被检测到。

  • 甚至我们可以计算出一个漏报概率公式。 x/a -1 (代表卡顿时间,a代表轮训时间)。也就是说如果我们要检测一个500ms的卡顿,轮训间隔时间要小于等于250ms。

  • 利用这种方式用来检测ANR也有很多弊端和不科学之处,这点我们在三板斧的第三篇文章里讲。

2 AndroidPerformanceMonitor 方案

2.1 基本原理

能查到到的最早的方案来源于开源:AndroidPerformanceMonitor(BlockCanary)其核心的原理在

  #Looper
private static boolean loopOnce(final Looper me,..) {
        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);
        }
        ...
        msg.target.dispatchMessage(msg);
        ...
        if (logging != null) {
            logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
        }
        ...
        msg.recycleUnchecked();
        return true;
   }

Looper 在分发执行每个Message 前后都会打印调用logging打印日志,且可喜的是我们设置该logging。依次可以计算出主线程的任务执行的耗时,超出【阈值】就认为出现了卡顿,进而收集信息。

我们在三板斧的第一篇里hook Choreographer 方案监控帧率方案里也使用了这一手段。

另外,Android api10 以上Looper 里添加了Observer,通过这一api我们也可以监控Message 的调度耗时。只不过该api是@hide的我们需要通过元反射的方式处理,对此这里不再展开。

2.2 方案监控盲区

该方案我们能监控到大部分的主线程耗时方法,还有少数的执行任务监控不到。

  • idleHandler的耗时监控不到
  • native层面发起的主线程任务监控不到。在java层面上我们拿到的堆栈是从 MessageQueue.java 的nativePollOnce(ptr, nextPollTimeoutMillis) 处发起,对应的native层面会执行然后回到java层,如inputEvent事件。
  • SyncBarrier泄漏问题导致的耗时。Looper中post的一个SyncBarrier,此后MessageQueue 里会跳过同步消息,只执行异步消息。当本该remove掉的SyncBarrier没被remvoe 掉的时候,同步消息永远得不到执行造成同步消息等待耗时。

下面我们展开针对这三个问题的解决方案。

3 进化完善方案

针对AndroidPerformanceMonitor 监控的三个主要盲区,一一补充完善方案。

3.1 idleHandler 耗时监控

#Looper
    public static void loop() {
        for (;;) {
            Message msg = queue.next(); //内部可能处理了IdleHandler
            ...
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                    msg.callback + ": " + msg.what);
            }
            ...
            msg.target.dispatchMessage(msg);
            ...
            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }
    }
  
# MessageQueue
  Message next() {
        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (; ; ) {
            nativePollOnce(ptr, nextPollTimeoutMillis);
            ...//取出message
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                //处理idleHandler
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null;
                boolean keep = false;
                keep = idler.queueIdle();
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }
        }
    }

如上代码,IdleHandler处理是在MessageQueue 里的next方法里。我们使用Looper 的printer 方案是监控不到的。

针对这个问题的解决方案,反射设置MessagQuueue 的private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>();成员变量,实现一个我们自身ArrayList,当往里添加IdleHandler时,讲起包装在我们实现的代理IdleHandlerWrapper里,IdleHandlerWrapper主要的工作就是在queueIdle();方法前后加入监控代码,监控queueIdle的执行耗时。

class IdleHandlerWrapper(
    val orgIdleHandler: MessageQueue.IdleHandler,
    private val callback: (Long) -> Unit
) : MessageQueue.IdleHandler {
    override fun queueIdle(): Boolean {
        val startTime = System.currentTimeMillis()
        val ret = orgIdleHandler.queueIdle()
        val costTime = System.currentTimeMillis() - startTime
        if (costTime > 2_000) {
            callback.invoke(costTime)
        }
        return ret
    }
}

3.2 Native 层面发起的主线程任务耗时监控

先看一个张图

image-20230309102514703.png

如上图是一个Activity onTouchEvent()方法的执行栈,由此可以看出,本次的执行时从MesssageQueuenativePollOnce处开始执行;并不是我们惯常的Handler的dispatchMessage(msg)处执行而来。

究其原因是,native层把epoll唤醒同时调用了ViewRootImpl里的InputReceiver.onInputEvent()方法

Looper#loop()
    -> MessageQueue#next()
        -> MessageQueue#nativePollOnce()
            -> NativeMessageQueue#pollOnce() //注意,进入 Native 层
                -> Looper.cpp#pollOnce()
                    -> Looper.cpp#pollInner()
                        -> epoll_wait()

熟悉Handler机制中epoll机制的同学知道,当MessageQueue没有执行的时候主线程会堵塞在nativePollOnce()处,被唤醒的时候会从此处执行。注意: 被唤醒并不是一定在java层往MessgeQueue里插入消息时,native层也能唤醒,且native层在唤醒时也可能会有任务要执行。

/system/core/libutils/Lopper.cpp
int pollInner(int timeoutMillis){
        int result = POLL_WAKE;
        // step 1,epoll_wait 方法返回
        int eventCount = epoll_wait(mEpollFd, eventItems, timeoutMillis); 
        if (eventCount == 0) { // 事件数量为0表示,达到设定的超时时间
            result = POLL_TIMEOUT;
        }
        for (int i = 0; i < eventCount; i++) {
           if (seq == WAKE_EVENT_FD_SEQ)) {
                // step 2 ,清空 eventfd,使之重新变为可读监听的 fd
                awoken();
            } else {
                // step 3 ,保存自定义fd触发的事件集合
                mResponses.push(eventItems[i]);
            }
        }
        // step 4 ,执行 native 消息分发
        while (mMessageEnvelopes.size() != 0) {
            if (messageEnvelope.uptime <= now) { // 检查消息是否到期
                messageEnvelope.handler->handleMessage(message);
            }
        }
        // step 5 ,执行 自定义 fd 回调
        for (size_t i = 0; i < mResponses.size(); i++) {
            response.request.callback->handleEvent(fd, events, data);
        }
        return result;
    }

native第4步和第5步的都会有任务执行。这些任务执行有可能会回调到java层,如TouchEvent事件,传感器事件。

方案

针对这类事件的监控,Matrix 的方案是:通过PLT Hook,hook libinput.so中的recvfromsendto方法。

鉴于此类事件接触到的比较多的就是TouchEvent和传感器事件,特别是TouchEvent事件,我这里提供一种低成本的监控 TouchEvent 耗时的方案。

设置Window.Callback 代理然后监控

class TouchCostWindowCallback(private val windowCallback: Window.Callback) :
    Window.Callback by windowCallback {
    override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
        val start = System.currentTimeMillis()
        val rst = windowCallback.dispatchTouchEvent(event)
        val cost = System.currentTimeMillis() - start
        // 监控
        return rst
    }
}

应用层所有的input事件都是通过InputEventReceriver接收,然后再经ViewRootImpl分发,分发的时候要经过 Window.Callback,利用这一特性实现TouchEvent的监控。

3.3 SyncBarrier 泄漏监控

SyncBarrier是一种特殊的Message,其特点是target为空,反射拿到MessageQueue的成员变量mMessages,通过message的when和当前时间对比,超过了我们的阈值。

然后再配合发送一个异步消息和同步消息,如果同步消息能够执行,异步消息一直得不到执行,可以判定Message的调度没问题,SyncBarrier产生了泄漏。

解决的方式:可以直接把SyncBarrier移除掉。

4 结

  • 本文的卡顿监控其实是从主线程耗时角度从一下四个方面进行监控,覆盖主线程里任务的耗时。

    • Java层Message调度耗时,通过设置Looper 的 Printer 监控java层Message调度执行耗时。
    • IdleHanlder执行耗时,hook MessageQueue监控Idlehandler的耗时。
    • native层主线程任务耗时。
    • SyncBarrier 泄漏监控
  • 这个信息是让我们有数可依量化【卡顿】这一主观指标变得更为【客观】。

  • 同时,这个信息对于我们三板斧的【ANR监控】有非常重要的参考意义。

  • 本文涉及的实现的代码以上传至github github.com/drummor/Get…

流程性三板斧 APM必备

Android 流畅性三板斧之帧率监控juejin.cn/post/721780…

Android 流畅性三板斧之卡顿监控juejin.cn/post/721877…

Android 流畅性三板斧之ANR监控 (juejin.cn/post/722004…)

关注 点赞 鼓励,感谢 !!!

本文正在参加「金石计划」