【android每日一问】怎么检测UI卡顿?(线上及线下)

79 阅读3分钟

线上UI卡顿检测方案

线上检测方案比较流行的是BlockCanary和WatchDog,下面我们就看看它们是怎么做到检测UI卡顿的并反馈给开发人员。

BlockCanary

  • BlockCanary能检测到主线程的卡顿, 并将结果记录下来, 以友好的方式展示,很类似于LeakCanary的展示。

BlockCanary的使用很简单,只要在Application中进行设置一下就可以如下:

BlockCanary.install(this, new AppBlockCanaryContext()).start();

  • AppBlockCanaryContext继承自BlockCanaryContext是对BlockCanary中各个参数进行配置的类

可配置参数如下:

//卡顿阀值 int getConfigBlockThreshold(); boolean isNeedDisplay(); String getQualifier(); String getUid(); String getNetworkType(); Context getContext(); String getLogPath(); boolean zipLogFile(File[] src, File dest); //可将卡顿日志上传到自己的服务 void uploadLogFile(File zippedFile); String getStackFoldPrefix(); int getConfigDumpIntervalMillis();

  • 在某个消息执行时间超过设定的标准时会弹出通知进行提醒,或者上传。

原理

熟悉Android的Handler机制的同学一定知道,Handler中重要的组成部分,looper,并且应用的主线程只有一个Looper存在,不管有多少handler,最后都会回到这里。 我们注意到Looper.loop()中有这么一段代码:

public static void loop() { ...

for (;;) { ...

// This must be in a local variable, in case a UI event sets the logger 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); }

... } }

注意到两个很关键的地方是logging.println(">>>>> Dispatching to " + msg.target + " " + msg.callback + ": " + msg.what);logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);这两行代码,它调用的时机正好在dispatchMessage(msg)的前后,而主线程卡也就是在dispatchMessage(msg)卡住了。

BlockCanary的流程图

(图片来自网络)

blockcanary_flow.png

BlockCanary就是通过替换系统的Printer来增加了一些我们想要的堆栈信息,从而满足我们的需求。

替换原有的Printer是通过以下方法:

Looper.getMainLooper().setMessageLogging(mainLooperPrinter);

并在mainLooperPrinter中判断start和end,来获取主线程dispatch该message的开始和结束时间,并判定该时间超过阈值(如2000毫秒)为主线程卡慢发生,并dump出各种信息,提供开发者分析性能瓶颈。如下所示:

@Override public void println(String x) { if (!mStartedPrinting) { mStartTimeMillis = System.currentTimeMillis(); mStartThreadTimeMillis = SystemClock.currentThreadTimeMillis(); mStartedPrinting = true; startDump(); } else { final long endTime = System.currentTimeMillis(); mStartedPrinting = false; if (isBlock(endTime)) { notifyBlockEvent(endTime); } stopDump(); } }

private boolean isBlock(long endTime) { return endTime - mStartTimeMillis > mBlockThresholdMillis; }

  • BlockCanary dump的信息包括如下:

基本信息:安装包标示、机型、api等级、uid、CPU内核数、进程名、内存、版本号等 耗时信息:实际耗时、主线程时钟耗时、卡顿开始时间和结束时间 CPU信息:时间段内CPU是否忙,时间段内的系统CPU/应用CPU占比,I/O占CPU使用率 堆栈信息:发生卡慢前的最近堆栈,可以用来帮助定位卡慢发生的地方和重现路径

  • 获取系统状态信息是通过如下代码实现:

threadStackSampler = new ThreadStackSampler(Looper.getMainLooper().getThread(), sBlockCanaryContext.getConfigDumpIntervalMillis()); cpuSampler = new CpuSampler(sBlockCanaryContext.getConfigDumpIntervalMillis());

下面看一下ThreadStackSampler是怎么工作的?

protected void doSample() { // Log.d("BlockCanary", "sample thread stack: [" + mThreadStackEntries.size() + ", " + mMaxEntryCount + "]"); StringBuilder stringBuilder = new StringBuilder();

// Fetch thread stack info for (StackTraceElement stackTraceElement : mThread.getStackTrace()) { stringBuilder.append(stackTraceElement.toString()) .append(Block.SEPARATOR); } // Eliminate obsolete entry synchronized (mThreadStackEntries) { if (mThreadStackEntries.size() == mMaxEntryCount && mMaxEntryCount > 0) { mThreadStackEntries.remove(mThreadStackEntries.keySet().iterator().next()); } mThreadStackEntries.put(System.currentTimeMillis(), stringBuilder.toString()); } }

直接去拿主线程的栈信息, 每半秒去拿一次, 记录下来, 如果发生卡顿就显之显示出来 拿CPU的信息较麻烦, 从/proc/stat下面拿实时的CPU状态, 再从/proc/" + mPid + "/stat中读取进程时间, 再计算各CPU时间占比和CPU的工作状态.

基于系统WatchDog原理来实现

  • 启动一个卡顿检测线程,该线程定期的向UI线程发送一条延迟消息,执行一个标志位加1的操作,如果规定时间内,标志位没有变化,则表示产生了卡顿。如果发生了变化,则代表没有长时间卡顿,我们重新执行延迟消息即可。

public class WatchDog { private final static String TAG = "budaye"; //一个标志 private static final int TICK_INIT_VALUE = 0; private volatile int mTick = TICK_INIT_VALUE; //任务执行间隔 public final int DELAY_TIME = 4000; //UI线程Handler对象 private Handler mHandler = new Handler(Looper.getMainLooper()); //性能监控线程 private HandlerThread mWatchDogThread = new HandlerThread("WatchDogThread"); //性能监控线程Handler对象 private Handler mWatchDogHandler;

//定期执行的任务 private Runnable mDogRunnable = new Runnable() { @Override public void run() { if (null == mHandler) { Log.e(TAG, "handler is null"); return; } mHandler.post(new Runnable() { @Override public void run() {//UI线程中执行 mTick++; } }); try { //线程休眠时间为检测任务的时间间隔 Thread.sleep(DELAY_TIME); } catch (InterruptedException e) { e.printStackTrace(); } //当mTick没有自增时,表示产生了卡顿,这时打印UI线程的堆栈 if (TICK_INIT_VALUE == mTick) { StringBuilder sb = new StringBuilder(); //打印堆栈信息 StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace(); for (StackTraceElement s : stackTrace) { sb.append(s.toString() + "\n"); } Log.d(TAG, sb.toString()); } else { mTick = TICK_INIT_VALUE; }