BlockCanary解析

BlockCanary

BlockCanary是markzhai同学的一个很实用的作品, 能检测到主线程的卡顿, 并将结果记录下来, 以友好的方式展示, 实属性能监测的良品, 他重用了LeakCanary的UI展示, 其它与LeakCanary的关系并不是太大.

BlockCanary使用

BlockCanary使用方式也比较简单, 要在Application中进行设置一下就可以了:

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

其中的AppBlockCanaryContext继承自BlockCanaryContext, 是对BlockCanary中各个参数进行配置的类, 我们可以重写其中的getXXX()方法来对其进行配置. 其中或配置的参数有很多, 看名字就可以知道了:

//卡顿阀值
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();

然后就可以进行操作了, 在某个消息执行时间超过设定的标准时会弹出通知进行提醒.

BlockCanary分析

BlockCanary的检测原理, 作者已经进行了说明, 就是在主线程消息循环打出日志的地入手, 当一个消息操作时间超过阀值后, 记录系统各种资源的状态, 并展示出来. 下面从代码的层面学习一下.
还是从Application的install开始分析,

public static BlockCanary install(Context context, BlockCanaryContext blockCanaryContext) {
    BlockCanaryContext.init(context, blockCanaryContext);
    setEnabled(context, DisplayBlockActivity.class, BlockCanaryContext.get().isNeedDisplay());
    return get();
}

其中的get()方法是BlockCanary作为单例向外提供拿实例的方法, 在第一次取实例的时候构造对象, 构造时创建一个BlockCanaryCore类型的成员, 也是类似单例模式, 在构造BlockCanaryCore的时候进行了初始化工作:

public BlockCanaryCore() {
    threadStackSampler = new ThreadStackSampler(Looper.getMainLooper().getThread(),
            sBlockCanaryContext.getConfigDumpIntervalMillis());
    cpuSampler = new CpuSampler(sBlockCanaryContext.getConfigDumpIntervalMillis());
    setMainLooperPrinter(new LooperPrinter(new BlockListener() {
        @Override
        public void onBlockEvent(long realTimeStart, long realTimeEnd,
                                 long threadTimeStart, long threadTimeEnd) {
            // Get recent thread-stack entries and cpu usage
            ArrayList<String> threadStackEntries = threadStackSampler
                    .getThreadStackEntries(realTimeStart, realTimeEnd);
            // Log.d("BlockCanary", "threadStackEntries: " + threadStackEntries.size());
            if (!threadStackEntries.isEmpty()) {
                Block block = Block.newInstance()
                        .setMainThreadTimeCost(realTimeStart, realTimeEnd, threadTimeStart, threadTimeEnd)
                        .setCpuBusyFlag(cpuSampler.isCpuBusy(realTimeStart, realTimeEnd))
                        .setRecentCpuRate(cpuSampler.getCpuRateInfo())
                        .setThreadStackEntries(threadStackEntries)
                        .flushString();
                LogWriter.saveLooperLog(block.toString());

                if (getContext().isNeedDisplay() && mOnBlockEventInterceptor != null) {
                    mOnBlockEventInterceptor.onBlockEvent(getContext().getContext(), block.timeStart);
                }
            }
        }
    }, getContext().getConfigBlockThreshold()));
    LogWriter.cleanOldFiles();
}

其中创建两个真正去拿系统状态的对象:threadStackSampler和cpuSampler.
创建一个LooperPrinter的对象设置给系统, 被系统当作记录日志Printer使用.
系统会在执行每个消息的前后都会调用Printer的println方法来记录日志, 下面看看LoopPrinter中是怎么写的.

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();
    }
}

从消息执行开始startDump(), 开始记录系统的各种资源状态, 在下次执行时, 也就是消息执行结束时判断是否达到了卡顿的阀值, 如果达到通知监听(在构造时传入)已经卡顿,同时停止记录系统资源状态. 其中的开始记录和结束记录都是直接调用了构造BlockCanaryCore时创建的两个监视资源的成员.
卡顿的监听也是在BlockCanaryCore构造时创建的, 他在收到卡顿通知后取得资源监视器中存储的数据, 存储到外存, 最后通知mOnBlockEventInterceptor, 而这个成员是什么时候传入的呢? 答案是在构造BlockCanary的时候, 先得到BlockCanaryCore实例, 然后就是初始化通知. 这里是通过反射取得Notifier的实例, 并将之设置为BlockCanaryCore中的mOnBlockEventInterceptor成员. 在Notifier中就直接弹出通知, 展示数据了.
下面看看两个资源监控的两个对象是怎么工作的, 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的工作状态. 下面两张图是BlockCanary的Demo中得到的实例, 看起来和LeakCanary挺像, 其实数据完全不一样

总述和CPU信息
主线程的栈信息

尾巴

有听说UC通过在Handler中放入idel消息来检测每条消息的耗时, 相比之下这种方式要容易理解得多, 还听说这个项目已经运用在多个实际项目中, 别等了, 快点加入吧, 哪怕是在自己本地跑一下也能对自己系统的卡顿有个客观的了解.

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 144,005评论 18 618
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 106,267评论 12 127
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 23,938评论 30 468
  • 爱分为很多种,有单纯的爱情,温暖的亲情,好玩的友情.......我在思考我们做一件事的时候是不是都有一定的爱,应该...
    小伙伴们一起交流阅读 43评论 0 0