王尘宇王尘宇

研究百度干SEO做推广变成一个被互联网搞的人

APP卡顿检测工具 BlockCanary——使用和原理

APP卡顿检测工具 BlockCanary——使用和原理

流程图

BlockCanary启动一个线程负责保存UI线程当前堆栈信息,将堆栈信息以及CPU信息保存分别保存在 mThreadStackEntries和mCpuInfoEntries中,每条信息都以时间撮为key保存。

BlockCanary注册了logging来获取事件开始结束时间。如果检测到事件处理时间超过阈值(默认值1s),则从mThreadStackEntries中查找T1T2这段时间内的堆栈信息,并且从mCpuInfoEntries中查找T1T2这段时间内的CPU及内存信息。并且将信息格式化后保存到本地文件,并且通知用户。
该组件利用了主线程的消息队列处理机制,通过

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;
    } else {
        final long endTime = System.currentTimeMillis();
        mStartedPrinting = false;
        if (isBlock(endTime)) {
            notifyBlockEvent(endTime);
        }
    }
}

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

四、源码解读

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

首先我们看看他的入口,install这个方法:

 /**
     * Install {@link BlockCanary}
     *
     * @param context            Application context
     * @param blockCanaryContext BlockCanary context
     * @return {@link BlockCanary}
     */
    public static BlockCanary install(Context context, BlockCanaryContext blockCanaryContext) {
        BlockCanaryContext.init(context, blockCanaryContext);
        setEnabled(context, DisplayActivity.class, BlockCanaryContext.get().displayNotification());
        return get();
    }

这里调用三行代码:

  • 调用init()方法, 记录ApplicationBlockCanaryContext, 为后面的处理提供上下文Context和配置参数(例如: 卡顿阈值,是否显示通知 等等…)
  • 调用setEnabled()方法, 判断桌面是否显示黄色的logo图标
  • 调用get()方法, 创建BlockCanary的实例,并且创建BlockCanaryInternals实例, 赋值给mBlockCanaryCore属性, 用来处理后面的流程
static void init(Context context, BlockCanaryContext blockCanaryContext) {
        sApplicationContext = context;
        sInstance = blockCanaryContext;
    }

这个init方法就做了一个赋值的操作,将我们传递过来的context进行赋值。

我们继续看BlockCanary.start()做了什么事:

public void start() {
    if (!mMonitorStarted) {
        mMonitorStarted = true;
        Looper.getMainLooper().setMessageLogging(mBlockCanaryCore.monitor);
    }
}

start()方法只做了一件事: 给Looper设置一个Printer

那么当Looper处理消息的前后, 就会调用mBlockCanaryCore.monitor的println()方法。

mBlockCanaryCore.monitor是BlockCanaryInternals的成员属性LooperMonitor

class LooperMonitor implements Printer {
    ...
    @Override
    public void println(String x) {
        //如果StopWhenDebugging, 就不检测
        if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
            return;
        }
        if (!mPrintingStarted) {
            mStartTimestamp = System.currentTimeMillis();
            mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
            mPrintingStarted = true;
            startDump();  //在子线程中获取调用栈和CPU信息
        } else {
            final long endTime = System.currentTimeMillis();
            mPrintingStarted = false;
            if (isBlock(endTime)) {  //判断是否超过设置的阈值
                notifyBlockEvent(endTime);
            }
            stopDump(); //停止获取调用栈和CPU信息
        }
    }
    //判断是否超过设置的阈值
    private boolean isBlock(long endTime) {
        return endTime - mStartTimestamp > mBlockThresholdMillis;
    }
    ...
}

LooperMonitor的println()就是最核心的地方, 实现代码也很简单:

  • Looper处理消息前, 获取当前时间并且保存, 调用startDump()启动一个任务定时去采集 调用栈/CPU 等等信息
  • Looper处理消息完成, 获取当前时间, 判断是否超过我们自定义的阈值isBlock(endTime)如果超过了, 就调用notifyBlockEvent(endTime)来通知处理后面的流程
  • 调用stopDump()停止获取调用栈以及CPU的任务

startDump采集的信息包括:

  • 基本信息:机型, CPU内核数, 进程名, 内存, 版本号 等等
  • 耗时信息:实际耗时, 主线程时钟耗时, 卡顿开始时间和结束时间
  • CPU信息:时间段内CPU是否忙, 时间段内的系统CPU/应用CPU占比, I/O占- – CPU使用率
  • 堆栈信息:发生卡顿前的最近堆栈

五、总结

blockcanary完美利用了安卓上的消息机制,给Looper设置一个Printer,通过记录堆栈和CPU信息,计算主线程处理消息的时间,如果超过了阈值,就检索此时的堆栈和cpu信息来帮助分析卡顿原因。

BlockCanary — 轻松找出Android App界面卡顿元凶
GitHub:BlockCanary

相关文章

评论列表

发表评论:
验证码

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。