Android UI卡顿检测(一)——基于Handler机制的实现方案(线上方案)

本文我们来分析Android UI的卡顿性能监控,本方案是基于Handler机制实现的,其他方案我们将在今后文章中进行分析。

UI卡顿产生的原因及监控方案分析


在Android中,UI线程负责执行UI视图的布局、渲染等工作,UI在更新期间,如果UI线程执行时间超过了16ms,则就会产生丢帧的现象,大量的丢帧,就会造成卡顿,影响用户体验。

Android中规定,每秒可以执行60次屏幕刷新,当我们的APP能够达到60帧/秒时,这种体验是优秀的,当帧率降低到40帧以下,甚至30帧以下,用户就可以感知到卡顿了。

UI卡顿产生的原因

UI卡顿通常产生的原因如下:

  • 系统CPU资源紧张,分配给APP主线程(UI线程)的CPU时间片减少。
  • UI线程中执行了大量的耗时任务,导致了UI线程视图刷新工作的阻塞。
  • Android虚拟机频繁执行GC操作导致的卡顿。由于GC会占用大量的系统资源,同时GC过程中会产生UI线程停顿,从而产生卡顿。
  • 过度绘制产生卡顿。过度绘制会导致GPU执行时间变长,从而产生丢帧现象。

在诸多原因中,大部分的原因是我们的编码导致的,这类问题可以通过各种优化手段进行优化。我们想要优化UI的性能,避免卡顿产生,首先我们必须做到监控卡顿的发生,能准确定位到哪个模块,甚至哪个方法导致了卡顿,UI性能问题也就解决了一大半了。

本文重点分析,UI线程执行大量耗时操作产生卡顿的检测手段,当然我们可以在线下通过AndroidStudio提供的检测工具进行检测,但我们更想监控线上用户的真实使用场景中的卡顿问题。

方案分析

思路

想要监控线上用户UI线程的卡顿,也就是要把UI线程中的耗时逻辑找出来,然后进行优化开发。那么我们如何如做呢?

Android中的应用程序是消息驱动的,也就是UI线程执行的所有操作,通常都会经过消息机制来进行传递(也就是Handler通信机制)。

Handler的handleMessage负责在UI线程中处理UI相关逻辑,如果我们能在handleMessage执行之前和handleMessage执行之后,分别插入一段我们的日志代码,不就可以实现UI任务执行时间的监控了吗?

方案设想?

我们要直接创建一个基类放在我们的项目代码中,所有需要Handler的地方都对此进行继承,然后我们在基类中添加日志监控,这样就可以实现我们的目的了吧?

NO! NO! NO!

首先这样对项目改造的成本太高了,而且我们也监控不到系统中的消息,也监控不到第三方sdk中的消息执行时间!

怎么做呢?

可行方案

还记得我们在前文 Handler线程通信机制:实战、原理、性能优化! 吗?,文中介绍了Handler的通信原理以及源码,既然所有的操作都要经过这里,我们是否可以从源码角度找到实现方案呢?

答案是肯定的!可行方案就在Handler机制的源码中。

UI卡顿检测的实现


我们来看Looper的loop方法:

    public static void loop() {
        ……
        for (;;) {
            ……
            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);
            }

            ……
        }
    }

loop方法中有一个Printer类型的logging,它会在消息执行之前和消息执行之后,输出一行日志,用于标记消息执行的开始和结束。

我们只要记录开始日志和结束日志的时间差,就可以计算出该任务在UI线程的执行时间了,如果执行时间很长,则必然产生了卡顿。

那么,问题来了,我们如何监控这个Printer类型的日志呢?

Printer的替换

    private Printer mLogging;
    public void setMessageLogging(@Nullable Printer printer) {
        mLogging = printer;
    }

我们发现mLogging这个对象可以通过一个public方法进行设置!这简直太好了!我们可以通过setMessageLogging方法设置我们自己的Printer对象就可以实现卡顿的监控了!

卡顿监控代码的实现

public class HandlerBlockTask {
    private final static String TAG = "budaye";
    public final int BLOCK_TMME = 1000;
    private HandlerThread mBlockThread = new HandlerThread("blockThread");
    private Handler mHandler;

    private Runnable mBlockRunnable = new Runnable() {
        @Override
        public void run() {
            StringBuilder sb = new StringBuilder();
            StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
            for (StackTraceElement s : stackTrace) {
                sb.append(s.toString() + "\n");
            }
            Log.d(TAG, sb.toString());
        }
    };
    public void startWork(){
        mBlockThread.start();
        mHandler = new Handler(mBlockThread.getLooper());
        Looper.getMainLooper().setMessageLogging(new Printer() {
            private static final String START = ">>>>> Dispatching";
            private static final String END = "<<<<< Finished";

            @Override
            public void println(String x) {
                if (x.startsWith(START)) {
                    startMonitor();
                }
                if (x.startsWith(END)) {
                    removeMonitor();
                }
            }
        });
    }

    private void startMonitor() {
        mHandler.postDelayed(mBlockRunnable, BLOCK_TMME);
    }
    private void removeMonitor() {
        mHandler.removeCallbacks(mBlockRunnable);
    }
}
逻辑解析:
  • Demo中,我们使用了一个工作线程mBlockThread来监控UI线程的卡顿。
  • 每次Looper的loop方法对消息进行处理之前,我们添加一个定时监控器。
  • 如果UI线程中的消息处理时间小于我们设定的阈值BLOCK_TMME,则取消已添加的定时器。
  • 当UI线程执行耗时任务,超过我们设定的阈值时,就会执行mBlockRunnable这个Rnnable,在它的run方法中,打印出主线程卡顿时的代码堆栈。
  • 我们把堆栈日志收集起来,进行归类分析,就可以定位到产生卡顿问题的具体代码行号了。

注:当然,你也可以打印出每个消息执行的具体时间,这也非常简单,不做具体Demo分析了。

总结


本章我们介绍了UI线程卡顿产生的原因,以及实现方案的分析。我们使用Handler机制实现了UI卡顿的监控,并且分析了实现原理,最后使用具体Demo完成了代码方案的实现。

当然,UI卡顿监控手段多种多样,我会在后面的文章中逐一进行分享。

你可能感兴趣的:(Android性能)