性能优化专题十三--BlockCanary简析

BlockCanary使用:

build.gradle添加BlockCanary依赖,如下:

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

在Application中注册:

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

注意AppBlockContext是我们继承自BlockCanaryContext,重写其中的一些方法,就可以定制我们自己的一些阈值,例如卡顿阈值我们设置为3秒,卡顿输出路径等等:

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) {

    }
}

Looper 提供的机制

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

在Looper的loop方法中,有一个Printer,它在每个Message处理的前后被调用,而如果主线程卡住了,就是 dispatchMessage里卡住了,里面实现的功能就是不断地从 MessageQueue 里面取出 Message 对象,并加以执行。

for (;;) {
    Message msg = queue.next(); // might block
    if (msg == null) {
        // No message indicates that the message queue is quitting.
        return;
    }

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

    // ignore some code...

    msg.recycleUnchecked();
}

注意到,在 dispatchMessage 的前后,分别有两个 log 的输出事件,而 dispatchMessage 就是线程上的一次消息处理。如果两次消息处理事件,都超过了 16.67ms, 那就一定发生了卡顿,这也是 BlockCanary 的基础原理。

1、获取主线程的Looper

因为Looper在每个线程最多只有一个实例,所以只要获取到主线程的Looper,就可以设置一个自定义的Printer对象到里面。

Looper mainLooper = Looper.getMainLooper();

2、创建自定义Printer

在Printer的println方法去计算主线程一条Message处理的时长,当时长超过设定的阈值时就判定是卡顿了。

BlockCanary 实现了 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();
        }
    }

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

  // ignore other codes...
}

这里实现了 println 方法,而 Looper 中也调用了 println 方法,而且在除非 dump 日志的情况下,也只有在事件消息前后进行 println 操作。换而言之,我们可以初步认为两个 println 调用之间的时间超过 16.67ms 就证明了卡顿。上面的代码也非常地清晰明了说明了这点。

3、设置自定义Printer到主线程Looper

Looper.getMainLooper().setMessageLogging(mainLooperPrinter);

BlockCanary源码分析:

 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这个方法就是决定开启或者关闭:

这里默认返回的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的间隔时间

2) 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是如何实现的:

我们点击LooperMonitor继续查看:

这个类实现了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是不是就是我们前面的监听时间回调?

到这里是不是就把我们要打印的数据通过这个回调方法返回去了.所以到这里我们的分析就全部完成了。

BlockCanary卡顿检测流程图

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

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

https://blog.csdn.net/wk_beicai/article/details/104609745/

猜你喜欢

转载自blog.csdn.net/cpcpcp123/article/details/106983922