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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容

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