美文网首页Android OtherAndroid 360性能调优 性能优化
4.Android 3分钟手写国内首个BlockCanary升

4.Android 3分钟手写国内首个BlockCanary升

作者: 鹏城十八少 | 来源:发表于2021-07-09 16:31 被阅读0次

    从月薪3000到年薪60万。从专科生到深圳一线大厂。关注我就能达到大师级水平,这话我终于敢说了, 年薪60万不是梦!

    地址是这个,里面是blockCanary

    https://github.com/markzhai/AndroidPerformanceMonitor

    有个问题:这个代码已经4年前写的,没有更新过了

    检测工具:

    目前流行的ANR检测方案有开源的BlockCanary 、ANR-WatchDog、SafeLooper,

    新版本ANR检测工具:rabbit-client

    https://github.com/SusionSuc/rabbit-client

    还有根据谷歌原生系统接口监测的方案:FileObserver。

    BlockCanary分析

    // 仅在debug包启用BlockCanary进行卡顿监控和提示的话,可以这么用

    <pre style="margin: 8px 0px; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198); font-size: 0.8rem;">public class AppBlockCanaryContext extends BlockCanaryContext {

    //设置卡顿判断的阙值
    

    public int getConfigBlockThreshold() {
    return 500;
    }

    //是否需要显示卡顿的信息
    

    public boolean isNeedDisplay() {
    return BuildConfig.DEBUG;
    }

    //设置log保存在sd卡的目录位置
    

    public String getLogPath() {
    return "/blockcanary/performance";
    }
    }</pre>

    测试代码:

    <pre style="margin: 8px 0px; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198); font-size: 0.8rem;">
    public void imitateClick(View view) {
    SystemClock.sleep(3000);
    }

    </pre>

    blockcanany的使用

    https://www.jianshu.com/p/74b0a2de389a

    基本原理

    我们都知道Android应用程序只有一个主线程ActivityThread,这个主线程会创建一个Looper(Looper.prepare),而Looper又会关联一个MessageQueue,主线程Looper会在应用的生命周期内不断轮询(Looper.loop),从MessageQueue取出Message 更新UI。

    我们来看一个代码片段 Loop方法里面.有一个打印,计算2次打印的时间。

    <pre style="margin: 8px 0px; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198); font-size: 0.8rem;">public static void loop() {
    ...
    for (;;) {
    ...
    // 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);
    }
    ...
    }
    }</pre>

    msg.target其实就是Handler,看一下dispatchMessage的逻辑

    <pre style="margin: 8px 0px; background-color: rgb(43, 43, 43); color: rgb(169, 183, 198); font-size: 0.8rem;">/**

    • Handle system messages here. */ public void dispatchMessage(Message msg) {
      if (msg.callback != null) {
      handleCallback(msg);
      } else {
      if (mCallback != null) {
      if (mCallback.handleMessage(msg)) {
      return;
      }
      }
      handleMessage(msg);
      }
      }</pre>

    流程:

    1、通过handler.postMessage() 发送消息给主线程 。start()方法,给looper设置打印器

    2、sMainLooper.looper() 通过轮询器不断的轮询MessageQueue中的消息队列
    
    3、通过Queue.next() 获取需要的消息
    
    4、计算出调用dispatchMessage()方法中前后的时间差值
    
    5、通过T2-T1的时间差来判断是否超过设定好的时间差的阀值
    
    6、如果T2-T1 时间差 > 阀值 ,就dump 出information来定位Ui卡顿 
    

    BlockCanary :

    原理是这样,比较Looper两次处理消息的时间差,比如大于3秒,就认为卡顿了。找到这3秒里面的所有方法,并得到最后一个。

    不管是哪种回调方式,回调一定发生在UI线程。因此如果应用发生卡顿,一定是在dispatchMessage中执行了耗时操作。我们通过给主线程的Looper设置一个Printer,打点统计dispatchMessage方法执行的时间,如果超出阀值,表示发生卡顿,则dump出各种信息,提供开发者分析性能瓶颈。

    通过时间,找到对应的方法,方法通过堆栈找

    而不是计算每一个方法的耗时。app那么多方法!

    遇到的问题:不准确

    消息队列只有一条消息,隔了很久才有消息入队,这种情况应该是要处理的,BlockCanary是怎么处理的呢?

    卡顿检测有缺陷,正在运行的函数有可能并不是真正耗时的函数

    [图片上传失败...(image-afd606-1625819443569)]

    在卡顿的周期之内,应用确实发生了卡顿,但是获取到的卡顿信息可能会不准确,和我们的OOM一样,也就是最后的堆栈信息仅仅只是一个表象,并不是真正发生问题时的一个堆栈。下面,我们先看下如下的一个示意图:

    [图片上传失败...(image-891566-1625819443570)]

    假设主线程在T1到T2的时间段内发生了卡顿,卡顿检测方案获取卡顿时的堆栈信息是T2时刻,但是实际上发生卡顿的时刻可能是在这段时间区域内另一个耗时过长的函数,那么可能在我们捕获卡顿的时刻时,真正的卡顿时机已经执行完成了,所以在T2时刻捕获到的一个卡顿信息并不能够反映卡顿的现场,也就是最后呈现出来的堆栈信息仅仅只是一个表象,并不是真正问题的藏身之处。

    那么,我们如何对这种情况进行优化呢?

    我们可以获取卡顿周期内的多个堆栈,而不仅仅是最后一个,这样的话,如果发生了卡顿,我们就可以根据这些堆栈信息来清晰地还原整个卡顿现场。因为我们有卡顿现场的多个堆栈信息,我们完全知道卡顿时究竟发生了什么,到底哪些函数它的调用时间比较长。接下来,我们看看下面的卡顿检测优化流程图:

    解决办法:

    BlockCanary 能做到连续调用几个方法也能准确揪出耗时是哪个方法,

    这个我在BlockCanary 中测试,并没有出现此问题,所以BlockCanary 是怎么处理的,简单分析一下源码:
    上面这段代码,注释1和注释2,记录第一次处理的时间,同时调用startDump()方法,startDump()最终会通过Handler 去执行一个AbstractSampler 类的mRunnable,代码如下:

    abstract class AbstractSampler {
    
        private static final int DEFAULT_SAMPLE_INTERVAL = 300;
    
        protected AtomicBoolean mShouldSample = new AtomicBoolean(false);
        protected long mSampleInterval;
    
        private Runnable mRunnable = new Runnable() {
            @Override
            public void run() {
                doSample();
                //调用startDump 的时候设置true了,stop时设置false
                if (mShouldSample.get()) {  
                    HandlerThreadFactory.getTimerThreadHandler()
                            .postDelayed(mRunnable, mSampleInterval);
                }
            }
        };
    
    

    可以看到,调用doSample之后又通过Handler执行mRunnable,等于是循环调用doSample,直到stopDump被调用。

    doSample方法有两个类实现,StackSampler和CpuSampler,分析堆栈就看StackSamplerdoSample方法

    protected void doSample() {

            StringBuilder stringBuilder = new StringBuilder();
            // 获取堆栈信息
            for (StackTraceElement stackTraceElement : mCurrentThread.getStackTrace()) {
                stringBuilder
                        .append(stackTraceElement.toString())
                        .append(BlockInfo.SEPARATOR);
            }
    
            synchronized (sStackMap) {
                // LinkedHashMap中数据超过100个就remove掉链表最前面的
                if (sStackMap.size() == mMaxEntryCount && mMaxEntryCount > 0) {
                    sStackMap.remove(sStackMap.keySet().iterator().next());
                }
                //放入LinkedHashMap,时间作为key,value是堆栈信息
                sStackMap.put(System.currentTimeMillis(), stringBuilder.toString());
            }
        }
    
    
    是因为开启循环去获取堆栈信息并保存到LinkedHashMap,因此不会出现误判或者漏判。核心代码就先分析到这里,其它细节大家可以自己去看源码。
    

    所以,BlockCanary 能做到连续调用几个方法也能准确揪出耗时是哪个方法,是因为开启循环去获取堆栈信息并保存到LinkedHashMap,因此不会出现误判或者漏判。

    第一次和第二次直接。里面的方时间和方法都记录下来。但是怎么知道这些方法是哪个方法呢?
    通过变量这个集合,找到时间差

    LooperMonitor

    public void println(String x) {

        if (mStopWhenDebugging && Debug.isDebuggerConnected()) {
    
            return;
    
        }
    
        if (!mPrintingStarted) {
    
            //1、记录第一次执行时间,mStartTimestamp
    
            mStartTimestamp = System.currentTimeMillis();
    
            mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
    
            mPrintingStarted = true;
    
            startDump(); //2、开始dump堆栈信息
    
        } else {
    
            //3、第二次就进来这里了,调用isBlock 判断是否卡顿
    
            final long endTime = System.currentTimeMillis();
    
            mPrintingStarted = false;
    
            if (isBlock(endTime)) {
    
                notifyBlockEvent(endTime);
    
            }
    
            stopDump(); //4、结束dump堆栈信息
    
        }
    
    }
    
    //判断是否卡顿的代码很简单,跟上次处理消息时间比较,比如大于3秒,就认为卡顿了
    
     private boolean isBlock(long endTime) {
    
        return endTime - mStartTimestamp > mBlockThresholdMillis;
    
    }
    

    demo地址:https://github.com/pengcaihua123456/shennandadao

    相关文章

      网友评论

        本文标题:4.Android 3分钟手写国内首个BlockCanary升

        本文链接:https://www.haomeiwen.com/subject/xcbfpltx.html