blockcanary源码解析(已完结)

一,背景:

我们在开发复杂项目的时候,代码的迭代,修改等,都会出现UI卡顿,或者出现ANR的时候,造成的程序崩溃,等,我们如何定位到卡顿的位置等,所以国内开发者,给我送来一个福利,BlockCanary这个框架。

二,blockCanary:

BlockCanary 这个框架是android平台,非侵入式的性能监控组件。使用时提供一个抽象类,传一个上下文环境就可以使用了,使用方便.

三,UI卡顿的原因:

在android开发中,我们的APP的帧频性能最优的目标就是保持在60fps上。
60fps --->16ms/帧
所以我们尽量保证每次在16ms内处理完所有的CPU与GPU计算,绘制,渲染等操作,否则会造成丢帧卡顿问题。

四:什么情况造成UI卡顿。

1)UI线程中做了一些耗时的操作。
解决办法:我们通过handler在子线程中做耗时操作
runOnUiThread方法:
View.post 方法
VIew.postDelayed方法
2)layout过于复杂,无法在16ms内完成。
3)View过度绘制。
4)view频繁的触发meeasure,layout。
5)内存频繁的触发GC。

五,blockCanary简单使

1)添加开源库的依赖:

 compile 'com.github.markzhai:blockcanary-android:1.5.0'

2)在Application中注册我们的BlockCanary

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

3).创建一个类AppBlockContext 继承 BlockCanaryContext:

public class AppBlockContext extends BlockCanaryContext {    // 实现各种上下文,包括应用标示符,用户uid,网络类型,卡慢判断阙值,Log保存位置等     /**     * Implement in your project.     *     * @return Qualifier which can specify this installation, like version + flavor.     */    public String provideQualifier() {        return "unknown";    }     /**     * Implement in your project.     *     * @return user id     */    public String provideUid() {        return "uid";    }     /**     * Network type     *     * @return {@link String} like 2G, 3G, 4G, wifi, etc.     */    public String provideNetworkType() {        return "unknown";    }     /**     * Config monitor duration, after this time BlockCanary will stop, use     * with {@code BlockCanary}'s isMonitorDurationEnd     *     * @return monitor last duration (in hour)     */    public int provideMonitorDuration() {        return -1;    }     /**     * Config block threshold (in millis), dispatch over this duration is regarded as a BLOCK. You may set it     * from performance of device.     *     * @return threshold in mills     */    public int provideBlockThreshold() {        return 1000;    }     /**     * Thread stack dump interval, use when block happens, BlockCanary will dump on main thread     * stack according to current sample cycle.     * <p>     * Because the implementation mechanism of Looper, real dump interval would be longer than     * the period specified here (especially when cpu is busier).     * </p>     *     * @return dump interval (in millis)     */    public int provideDumpInterval() {        return provideBlockThreshold();    }     /**     * Path to save log, like "/blockcanary/", will save to sdcard if can.     *     * @return path of log files     */    public String providePath() {        return "/blockcanary/";    }     /**     * If need notification to notice block.     *     * @return true if need, else if not need.     */    public boolean displayNotification() {        return true;    }     /**     * Implement in your project, bundle files into a zip file.     *     * @param src  files before compress     * @param dest files compressed     * @return true if compression is successful     */    public boolean zip(File[] src, File dest) {        return false;    }     /**     * Implement in your project, bundled log files.     *     * @param zippedFile zipped file     */    public void upload(File zippedFile) {        throw new UnsupportedOperationException();    }      /**     * Packages that developer concern, by default it uses process name,     * put high priority one in pre-order.     *     * @return null if simply concern only package with process name.     */    public List<String> concernPackages() {        return null;    }     /**     * Filter stack without any in concern package, used with @{code concernPackages}.     *     * @return true if filter, false it not.     */    public boolean filterNonConcernStack() {        return false;    }     /**     * Provide white list, entry in white list will not be shown in ui list.     *     * @return return null if you don't need white-list filter.     */    public List<String> provideWhiteList() {        LinkedList<String> whiteList = new LinkedList<>();        whiteList.add("org.chromium");        return whiteList;    }     /**     * Whether to delete files whose stack is in white list, used with white-list.     *     * @return true if delete, false it not.     */    public boolean deleteFilesInWhiteList() {        return true;    }     /**     * Block interceptor, developer may provide their own actions.     */    public void onBlock(Context context, BlockInfo blockInfo) {     }}

六,BlockCanary的原理源码实现

    在ActivityThread中有一个main方法,在main方法中会创建一个Looper,在Looper当中会关联一个MessageQueue消息队列,主线程创建好MainLooper之后,他就会在应用的生命周期内不断的轮训,通过Looper.loop方法。然后获取到我们消息队列当中的message,最后通知我们的主线程去更新UI.

2)实现的核心原理

     通过Hander.postMessage发送一个消息给主线程(sMainLooper.loop),主线程会通过轮训器Looper不断的轮训MessageQueue中的消息队列,通过queue.next方法获取消息队列中的消息,然后我们计算出调用dispatchMessage方法的前后时间值(T1,T2),通过T2减去T1的时间差来判断是否超过我们之前设定好的阈值,如果超过了我们设定的阈值,我们就dump出我们收集的信息,来定位我们UI卡顿的原因。

如果我们在调用dispatchmessage这个方法的时候,超过我们设定的阈值的0.8倍的时候,也会Dump出我们需要的信息。

3)我们通过源码进行分析

 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方法:

static void init(Context context, BlockCanaryContext blockCanaryContext) {        sApplicationContext = context;        sInstance = blockCanaryContext;    }

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

我们返回到install方法中。看setEnabled方法:

更据用户的通知栏消息,来开启或者关闭展示我们BlockCanary这个消息界面.

我们看这个方法的第三个参数,dispalyNotification这个方法就是决定开启或者关闭:

  /**     * If need notification to notice block.     *     * @return true if need, else if not need.     */    public boolean displayNotification() {        return true;    }

这里默认返回的true。如果是debug方法是true,relese返回false

我们在看看get方法的实现:

 /**     * Get {@link BlockCanary} singleton.     *     * @return {@link BlockCanary} instance     */    public static BlockCanary get() {        if (sInstance == null) {            synchronized (BlockCanary.class) {                if (sInstance == null) {                    sInstance = new BlockCanary();                }            }        }        return sInstance;    }

他其实就是一个单例模式,我们看看这个BlockCanary是如何实现的那?

 private BlockCanary() {        BlockCanaryInternals.setContext(BlockCanaryContext.get());        mBlockCanaryCore = BlockCanaryInternals.getInstance();        mBlockCanaryCore.addBlockInterceptor(BlockCanaryContext.get());        if (!BlockCanaryContext.get().displayNotification()) {            return;        }        mBlockCanaryCore.addBlockInterceptor(new DisplayService());     }

BlockCanaryInternals.setContext,做了一个赋值操作,

mBlockCanaryCode这个类型的变量就是BlockCanaryInternals,我们看一下getInstance方法:

/**     * Get BlockCanaryInternals singleton     *     * @return BlockCanaryInternals instance     */    static BlockCanaryInternals getInstance() {        if (sInstance == null) {            synchronized (BlockCanaryInternals.class) {                if (sInstance == null) {                    sInstance = new BlockCanaryInternals();                }            }        }        return sInstance;    }

一个单例模式完成了BlockCanaryInternals的实例化。

我们接着看addBlockInterceptor这行代码:

这是一个拦截器,传入一个上下文

主要代码,判断是否开启,展开这个拦截器,通知我们的DisplayActivity。

接下来我们看一看BlockCanaryInternals这个类(这个类是一个核心类)

 public BlockCanaryInternals() {         stackSampler = new StackSampler(                Looper.getMainLooper().getThread(),                sContext.provideDumpInterval());         cpuSampler = new CpuSampler(sContext.provideDumpInterval());         setMonitor(new LooperMonitor(new LooperMonitor.BlockListener() {             @Override            public void onBlockEvent(long realTimeStart, long realTimeEnd,                                     long threadTimeStart, long threadTimeEnd) {                // Get recent thread-stack entries and cpu usage                ArrayList<String> threadStackEntries = stackSampler                        .getThreadStackEntries(realTimeStart, realTimeEnd);                if (!threadStackEntries.isEmpty()) {                    BlockInfo blockInfo = BlockInfo.newInstance()                            .setMainThreadTimeCost(realTimeStart, realTimeEnd, threadTimeStart, threadTimeEnd)                            .setCpuBusyFlag(cpuSampler.isCpuBusy(realTimeStart, realTimeEnd))                            .setRecentCpuRate(cpuSampler.getCpuRateInfo())                            .setThreadStackEntries(threadStackEntries)                            .flushString();                    LogWriter.save(blockInfo.toString());                     if (mInterceptorChain.size() != 0) {                        for (BlockInterceptor interceptor : mInterceptorChain) {                            interceptor.onBlock(getContext().provideContext(), blockInfo);                        }                    }                }            }        }, getContext().provideBlockThreshold(), getContext().stopWhenDebugging()));         LogWriter.cleanObsolete();    }

我们看一下这个构造方法,和重要的三个变量:

1)stackSampleer

参数一:传入我们的主线程

参数二:Dump的间隔时间

  1. cpuSampler

他会dump出我们cup的一些情况

3)LooperMonitor

这是一个非常重要的东西,如何打印上下时间(T1,T2),就是通过它控制的,然后通过onBlockEvent回调监听并打印数据。

4)cleanObsolete这个方法就是删除我们打印的日志

上边讲解的是BlockCanary的install初始化的,接下来我们讲解start()的打印是如何打印的:

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

最关键的代码:就是Looper.getMainLooper这行代码:调取主线程的setMessageLogging方法,来打点我们时间。

我们接下来看看代码Monitor是如何实现的:

image

我们点击LooperMonitor继续查看:


image

这个类实现了Printer这个接口。

这个类中重要的方法是:

 @Override    public void println(String x) {        if (mStopWhenDebugging && Debug.isDebuggerConnected()) {            return;        }        if (!mPrintingStarted) {            mStartTimestamp = System.currentTimeMillis();            mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();            mPrintingStarted = true;            startDump();        } else {            final long endTime = System.currentTimeMillis();            mPrintingStarted = false;            if (isBlock(endTime)) {                notifyBlockEvent(endTime);            }            stopDump();        }    }

这个方法就是用来打点时间的,

首先看代码实现:

首先判断dispacthMessage这个方法之前调用的,如果是就会记录开始时间,调用这个startDump这个方法,来打印出我们的堆栈信息。

接下来我们看看这个startDump这个方法的实现:

private void startDump() {        if (null != BlockCanaryInternals.getInstance().stackSampler) {            BlockCanaryInternals.getInstance().stackSampler.start();        }         if (null != BlockCanaryInternals.getInstance().cpuSampler) {            BlockCanaryInternals.getInstance().cpuSampler.start();        }    }

这个方法主要就是通过BlockCanaryInternals中的stackSampler和cpuSampler分别打印出重要信息。

我们继续深入,看看他们的start方法实现:

public void start() {        if (mShouldSample.get()) {            return;        }        mShouldSample.set(true);         HandlerThreadFactory.getTimerThreadHandler().removeCallbacks(mRunnable);        HandlerThreadFactory.getTimerThreadHandler().postDelayed(mRunnable,                BlockCanaryInternals.getInstance().getSampleDelay());    }

这里没啥说的,主要看看postDelayed这个方法的第一个参数,mRunnalbe:

private Runnable mRunnable = new Runnable() {        @Override        public void run() {            doSample();             if (mShouldSample.get()) {                HandlerThreadFactory.getTimerThreadHandler()                        .postDelayed(mRunnable, mSampleInterval);            }        }    };

我们在看看doSample是什么?

  abstract void doSample();

这是一个抽象方法,这也就是意味着stackSampler和cpuSampler是有不同实现的,

我们接着看看这个抽象方法的StackSampler的实现:

  @Override    protected void doSample() {        StringBuilder stringBuilder = new StringBuilder();         for (StackTraceElement stackTraceElement : mCurrentThread.getStackTrace()) {            stringBuilder                    .append(stackTraceElement.toString())                    .append(BlockInfo.SEPARATOR);        }         synchronized (sStackMap) {            if (sStackMap.size() == mMaxEntryCount && mMaxEntryCount > 0) {                sStackMap.remove(sStackMap.keySet().iterator().next());            }            sStackMap.put(System.currentTimeMillis(), stringBuilder.toString());        }    }

到这里就是真正要打印的数据了:

我们看最后一行代码:执行了打印,第一个参数是以我们的当前时间戳为例,并放到HashMap当中,我们看看是什么HashMap?

private static final LinkedHashMap<Long, String> sStackMap = new LinkedHashMap<>();

他是一个linkHashMap: 为什么要用这个HashMap,因为这个LinkHashMap能够记录插入的顺序。

所以这里是按着先后顺序插入的,

我们回到这Printer这个接口的println方法:

 @Override    public void println(String x) {        if (mStopWhenDebugging && Debug.isDebuggerConnected()) {            return;        }        if (!mPrintingStarted) {            mStartTimestamp = System.currentTimeMillis();            mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();            mPrintingStarted = true;            startDump();        } else {            final long endTime = System.currentTimeMillis();            mPrintingStarted = false;            if (isBlock(endTime)) {                notifyBlockEvent(endTime);            }            stopDump();        }    }

我们看看这个isBlock这个方法:

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

到这里我们就明白了,这里是不是就是我们BlockCanary的核心原理,T2减去T1的时间,并判断是否打印并返回true,就会执行notifyBlockEvent,我们看看这个实现:

 private void notifyBlockEvent(final long endTime) {        final long startTime = mStartTimestamp;        final long startThreadTime = mStartThreadTimestamp;        final long endThreadTime = SystemClock.currentThreadTimeMillis();        HandlerThreadFactory.getWriteLogThreadHandler().post(new Runnable() {            @Override            public void run() {                mBlockListener.onBlockEvent(startTime, endTime, startThreadTime, endThreadTime);            }        });    }

我们看看这个方法中的LooperMonitor,this这个代码,onBlockEvent是不是就是我们前面的监听时间回调?

到这里是不是就把我们要打印的数据通过这个回调方法返回去了.所以到这里我们的分析就全部完成了。如果对您有帮助,麻烦关注我一下,我会给你继续带来干货。

前人总结:https://blog.csdn.net/wk_beicai/article/details/104609745/

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

推荐阅读更多精彩内容