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

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

谨慎的讲,帧率下降不一定会有卡顿(这儿对卡顿是从技术角度界说在主线程履行了耗时使命),卡顿发生的原因还有其他因素导致,比方系统负载、CPU繁忙等。

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

Android 流畅性三板斧之帧率监控(/post/721780…)

Android 流畅性三板斧之卡顿监控(/post/721877…)

Android 流畅性三板斧之ANR监控 (/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

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

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

  • 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 层面建议的主线程使命耗时监控

先看一个张图

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

如上图是一个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 流畅性三板斧之帧率监控(/post/721780…)

Android 流畅性三板斧之卡顿监控(/post/721877…)

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

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

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