美文网首页
BlockCanary 卡顿监测

BlockCanary 卡顿监测

作者: 艾瑞败类 | 来源:发表于2023-04-17 13:35 被阅读0次

    作者:海象

    前言

    最近在处理项目中的拍摄视频后上传界面卡顿的问题,找到 BlockCanary 这个工具来定位,由于不支持高版本 Android,当时在定位卡顿时先将项目的 targetSdk 版本降下来,当然这不是个长久的办法,打算花一点时间适配下高版本,先过一遍源码流程

    网上很多博客只提到适配分区存储和通知栏,好像忽略了一个细节,CPU 的采样"proc" 在高版本 Android 被禁用,原因是系统防止旁路攻击,只允许系统应用访问

    初始化

    BlockCanary 跟随 App 启动,内部的 BlockCanaryInternal 有添加拦截器的操作,这里应该是控制的核心逻辑

    install () 大概做了这些:

    1. BlockCanaryContext 实现了 BlockInterceptor 接口 进行基础设置,比如文件夹名,判断时间等,用于给开发者自定义的
    2. 启动组件,显示图标到桌面

    BlockCanary 构造方法中做了:

    1. 创建 BlockCanaryInternal
    2. 添加拦截器到 BlockCanaryInternal

    这里主要是为子线程监测做初始化

    启动

        public void start() {
            // 往主线程的 looper 里设置 printer
            Looper.getMainLooper().setMessageLogging(mBlockCanaryCore.monitor);
        }
    

    这里就是 BlockCanary 检测的位置,原理是 Looper.loop() 中会在 Looper.dispatchMessage() 执行前后做打印,刚好可以利用这个做执行时长的处理,通过判断是否超过时间,来判断是否发生了卡顿

    检测执行时长

    检测时长的逻辑位于 LooperMonitor,它实现了 Printer 接口

        @Override
        public void println(String x) {
    
            // 执行前
            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;
        }
    

    先来思考下,如果卡顿已经发生了,我们想要获取哪些信息来定位问题:

    1. 哪个位置发生了卡顿,我觉得这是最最重要的
    2. 发生卡顿的原因,是内存不够导致的,其他地方导致的,这对定位问题比较重要

    那这些信息应该是在卡顿后,再去获取吗,还能拿到现场信息嘛? 带着这些问题,来看看 BlockCanary 是怎么做的

    来看看 startDump() 中做了什么

        private void startDump() {
           // 分别调用了 StackSampler/CpuSampler 的 start()   
           BlockCanaryInternals.getInstance().stackSampler.start()
           BlockCanaryInternals.getInstance().cpuSampler.start();
        }
    

    这两个 start 都是在子线程中执行的,原因是基类内部有个 HandlerThread ,在这个子线程执行方法 doSample()

    获取当前执行的内存堆栈的逻辑就在这里,也是定位卡顿位置的关键

        // StackSampler
        @Override
        protected void doSample() {
    
            StringBuilder stringBuilder = new StringBuilder();
            // 遍历当前线程(主线程)的所有堆栈,放到 String 里
            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());
                }
                // 保存到 Map 中,最多保存 100 个
                sStackMap.put(System.currentTimeMillis(), stringBuilder.toString());
            }
        }
    
       // CpuSampler
    
        protected void doSample() {
            BufferedReader cpuReader = null;
            BufferedReader pidReader = null;
                // 通过 /proc/stat 读取 cpu 参数,这个 Android 高版本中已经被禁用了
                cpuReader = new BufferedReader(new InputStreamReader(
                        new FileInputStream("/proc/stat")), BUFFER_SIZE);
                String cpuRate = cpuReader.readLine();
    
                if (mPid == 0) {
                    mPid = android.os.Process.myPid();
                }
                pidReader = new BufferedReader(new InputStreamReader(
                        new FileInputStream("/proc/" + mPid + "/stat")), BUFFER_SIZE);
                String pidCpuRate = pidReader.readLine();
                if (pidCpuRate == null) {
                    pidCpuRate = "";
                }
    
                parse(cpuRate, pidCpuRate);
    
        }
    

    继续来看是怎样通知的,都通知了谁

        private void notifyBlockEvent(final long endTime) {
            // 触发 onBlockEvent,在子线程中执行
            HandlerThreadFactory.getWriteLogThreadHandler().post(new Runnable() {
                @Override
                public void run() {
                    mBlockListener.onBlockEvent(startTime, endTime, startThreadTime, endThreadTime);
                }
            });
        }
    

            @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) {
                           //在 BlockCanary 的构造函数里,添加了拦截器到队列中,主要是来打开 DisplayActivity,像开发者展示卡顿信息 interceptor.onBlock(getContext().provideContext(), blockInfo);
                            }
                        }
                    }
                }
    

    小结

    BlockCanary 核心是通过 Looper 中分发 Message 前后会执行的打印,在这个判断执行时长是否过长,如果判断为阻塞,就马上将执行前就开始收集的程序堆栈/CPU 内存信息在一个页面中展示出来,这里的收集都是在子线程中进行的

    既然在高版本上 “/proc/stat” 已经不能用了,我们能不能做个版本判断,在高版本上不去其获取 CPU 信息了呢?

    这样还是不太好,如果没有 CPU 使用频率这些信息,我们判断卡顿时就没法排查是 CPU 跑满了,分不到足够的时间片

    高版本获取 CPU 使用率

    不过“/proc/stat”由于在 API 26 以上,只有系统应用才能使用,这也让 BlockCanary 的 CPU 监测部分在高版本上不可使用了

    原因是会被利用来对系统旁路攻击,Android 禁止了非系统应用的访问

    思考

    1. 除了使用 Printer , Android 10 以上支持 Looper Observer,不过由于是 Hidden API,需要绕过限制
    2. Printer 是 Looper 中的成员变量,会不会存在被替换的风险,如果有其他库也使用了 Printer 会导致无法开始采样吧,个人疑问,有不同看法欢迎讨论

    相关知识

    • StackTrace 当前线程的堆栈信息,当时一个方法执行时,会对应创建一个栈帧,通过 StackTraceElement ,可以获取如下数据:
    1. 声明的类名
    2. 方法名
    3. 行号
    4. 文件名(不知道是什么)
    • 组件开关 通过 PackageManager.setComponentEnabledSetting,能够对组件的开关进行控制,BlockCanary 在 Launcher 的显示和隐藏就是通过这个设置的,关闭之后 DisplayActivity 的桌面图片也不再显示了

    相关文章

      网友评论

          本文标题:BlockCanary 卡顿监测

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