BlockCanary 卡顿监测

news2024/12/26 3:43:48

作者:海象

前言

最近在处理项目中的拍摄视频后上传界面卡顿的问题,找到 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 的桌面图片也不再显示了

除了卡顿监控之外,Android 还有以下几种性能监控方式:

  1. 内存监控:通过监控应用程序的内存使用情况,诊断内存溢出、内存泄漏等问题,并定位内存泄漏的位置。
  2. CPU 监控:通过监控应用程序的 CPU 使用率,诊断频繁占用 CPU 的代码段,并定位其所在位置。
  3. 网络监控:通过监控应用程序的网络请求和响应信息,分析网络请求频率、请求/响应时间、网络异常等问题,找出网络请求性能瓶颈点。
  4. 能耗监控:通过监控应用程序的能耗数据,诊断应用程序的能耗情况,找到耗电严重的代码片段,并对其进行优化。
  5. 稳定性监控:通过跟踪应用程序的崩溃日志,诊断应用程序的稳定性问题,包括 ANR、Crash 等,并找出引起崩溃的代码片段。

以上是 Android 中常用的性能监控方式,开发者可以根据自己的需求选择不同的监控工具和技术手段,以达到优化应用程序性能的目的。

Android 中常用的性能监控工具如下

  1. 内存监控:Android Studio 自带的 DDMS 工具、MAT(Memory Analyzer Tool)、LeakCanary 等。
  2. CPU 监控:Android Studio 自带的 Profiler 工具、Systrace、TraceView 等。
  3. 网络监控:Charles、Wireshark、tcpdump 等。
  4. 能耗监控:Battery Historian、Battery Usage 等。
  5. 稳定性监控:ACRA、Bugsnag、Firebase Crashlytics 等。

除了上述工具外,还有一些第三方性能监控集成平台,如 AppDynamics、New Relic、Dynatrace 等。这些平台可以对应用程序进行全面的性能监控,提供各种监控数据和分析报告,帮助开发者诊断和解决性能问题。

根据不同的性能监控问题,我们需要采用不同的性能优化手段,而目前还是有些人群对于性能优化中间的一些优化手段掌握的不是很熟练,因此针对性能优化中间的所有不同类型的优化手段进行了归类整理,有启动优化、内存优化、网络优化、卡顿优化、存储优化……等,整合成名为《Android 性能优化核心知识点手册》,大家可以参考下:

《APP 性能调优进阶手册》:https://qr18.cn/FVlo89

启动优化

内存优化

UI优化

网络优化

Bitmap优化与图片压缩优化

多线程并发优化与数据传输效率优化

体积包优化

《Android 性能调优核心笔记汇总》:https://qr18.cn/FVlo89

《Android 性能监控框架》:https://qr18.cn/FVlo89

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/426285.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ESP32 分区表

ESP32 分区表 1. 分区表概述 ESP32 针对 flash 进行划分&#xff0c;划分为不同的区域用作不同的功能&#xff0c;并在flash的 0x8000 位置处烧写了一张分区表用来描述分区信息。 分区表可以根据自己的需要进行配置&#xff0c;每一个分区都有其特定的作用&#xff0c;可根据…

有学生问我,重构是什么?我应该如何回答?

重构到底是什么&#xff1f;只是代码的推倒重新编码&#xff1f;还是有规则、有方法可寻&#xff1f;当然&#xff0c;结论肯定是有的&#xff0c;本文&#xff0c;我们通过一个简单的实例&#xff0c;来理解一下重构。 1.借助一个实例需求 这是一个影片出租店用的程序&#…

管理笔记2职级管理

一职级目的&#xff1a; 1 员工发展的职业通道 2 招聘、晋升的公平性 二 能力模型 类似职级 三 晋升 当前级别的事做到了&#xff0c;已经做到下一个级别的事 考虑点&#xff1a;1考虑当前阶段时间&#xff0c;不会频繁晋升&#xff0c;2考虑绩效。 不断成长、主动做事&a…

认证授权SpringSecurity

如何引入SpringSecurity作为项目的权限认证服务 1.引入依赖 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-security</artifactId> </dependency> <dependency><groupId>org.…

客快物流大数据项目(一百一十三):注册中心 Spring Cloud Eureka

文章目录 注册中心 Spring Cloud Eureka 一、Eureka 简介 二、架构图

不得不了解的linux网络配置

目录一、查看及测试网络1.1查看网络配置1.1.1ifconfig命令—查看网络接口地址1.1.2route命令—查看路由条目1.1.3hostname命令—查看主机名称1.1.4netstat命令—查看网络连接情况1.1.5 ss命令/lsof 命令— 获取socket(套接字)统计信息1.2测试网络连接1.2.1ping命令—测试网络连…

1 Nginx跨域配置

跨域问题在之前的单体架构开发中&#xff0c;其实是比较少见的问题&#xff0c;除非是需要接入第三方SDK时&#xff0c;才需要处理此问题。但随着现在前后端分离、分布式架构的流行&#xff0c;跨域问题也成为了每个Java开发必须要懂得解决的一个问题。 跨域问题产生的原因 产…

项目管理职业发展前景怎么样?

项目管理职业发展前景怎么样&#xff1f;我们可以从这三个问题出发—— 第一个问题&#xff1a;在中国「项目管理」的专业性的体现/认同度如何&#xff1f;缺少专业知识对未来工作选择的限制程度如何&#xff1f; 由于现在有不少跨专业从业者&#xff0c;他们通过自学考证等途…

计算广告(十二)

FFM模型 FFM&#xff08;Field-aware Factorization Machine&#xff0c;领域感知因子分解机&#xff09;是一种广泛应用于推荐系统和点击率预测&#xff08;CTR&#xff09;等任务的机器学习模型。 它是基于FM&#xff08;Factorization Machine&#xff0c;因子分解机&…

nuscenes instance 调研笔记

nuscenes instance 调研笔记&#xff1a; 前言 nuscenes这个自动驾驶数据集挺大&#xff0c;官方devkit还提供了各种操作用于提高科研人员的效率&#xff0c;但是吧&#xff0c;东西多了学起来就乱七八糟的&#xff0c;本文仅以提取每个instance关联的所有images为例子&#…

USRP具有MIMO系统如何同步

MIMO系统要求 时间和频率同步&#xff0c;并且每个通道必须满足下面两个要求&#xff1a; 1&#xff09;时钟必须同步对齐 2&#xff09;对DSP操作时间也要对齐&#xff0c;来自同一的时钟边缘 波束成形和测向要求 除了采样时间和采样时钟对准外&#xff0c;系统还必须在每个…

华为OD机试 - 相同数字组成图形的周长(Java JS Python)

题目描述 有一个6464的矩阵,每个元素的默认值为0,现在向里面填充数字,相同的数字组成一个实心图形,如下图所示是矩阵的局部(空白表示填充0): 数字1组成了蓝色边框的实心图形,数字2组成了红色边框的实心图形。 单元格的边长规定为1个单位。 请根据输入,计算每个非0值…

java 数组和字符串操作

目录一、前言二、数组操作1.charAt操作2.getBytes操作3.toCharArray操作4.String.valueOf5.substring&#xff0c;toUpperCase&#xff0c;toLowerCase&#xff0c;concat6.indexOf7.Arrays使用一、前言 本文章主要讲解数组的一些基本操作&#xff0c;让我们写代码更加方便&am…

1.docker-安装及使用

1.安装步骤 Install Docker Engine on CentOS 1. 确定CenOS7及以上版本 cat /etc/redhat-release2.卸载旧版本 yum remove docker \docker-client \docker-client-latest \docker-common \docker-latest \docker-latest-logrotate \docker-logrotate \docker-engine3.yum安…

软件测试岗位求职避坑,今年毁约应届生的公司有这么多?….

不知不觉九月已至&#xff0c;“金三银四”招聘旺季也落入尾声&#xff0c;大学生也迎来毕业季&#xff01; 这个时候&#xff0c;应届生开始集体走向社会&#xff0c;而职场人也迎来了跳槽涨薪的好时机。 可现实的求职情况却是一片困境&#xff1a;从中小微到“大厂”&#…

ICPC SWERC 2020 K - Unique Activities(SAM记录子串第一次出现的位置 or SAM + hash)

两种做法的效率差异 做法一&#xff1a;SAM记录子串第一次结束位置 做法二&#xff1a;SAM hash 题意&#xff1a; 给定一个字符串&#xff0c;让你找到只出现过一次&#xff0c;且长度最短的子串并输出&#xff0c;如果有多个则输出最先出现的那个。 思路&#xff1a…

迷宫问题-DFS-BFS

迷宫问题迷宫问题简介BFS解决迷宫最短路径问题DFS记录迷宫路径DFS解决迷宫所有路径问题迷宫问题简介 &#x1f680;学习过算法程序设计的应该都学习过迷宫这个问题&#xff0c;迷宫问题主要设计的算法就是DFS-深度优先遍历和BFS-广度优先遍历。 &#x1f680;在一个二维数组中…

Jmeter和Testlink自动化测试框架研究与实施

摘 要 目前基于Jmeter的接口自动化测试框架&#xff0c;大多只实现脚本维护和自动调度&#xff0c;无法与Testlink进行互通&#xff0c;实现测试方案与自动化实施流程连接&#xff0c;本文基于Testlink、Jmeter、Jenkins实现&#xff1a;通过Testlink统一维护接口自动化测试用…

搭建es集群

单点es的缺点&#xff1a;1.存储数据有限&#xff1b;2.单点故障问题&#xff08;es出现故障则整个服务会直接宕机&#xff09; 解决存储数据有限&#xff1a;搭建多台es服务器实现集群 解决单点故障&#xff1a;在不同的es服务器中进行备份数据&#xff08;例&#xff1a;在…

车载测试需要有哪些知识需要学习的?

一、车载行业前景 其中的车载测试也随着国家对新能源、智能驾驶等领域的支持&#xff0c;而异常活跃&#xff0c;目前我国共有9000家自动驾驶相关企业&#xff0c;而今年从华为、中兴、大唐等通信领域的企业到以阿里、腾讯、小米等为代表的互联网企业&#xff0c;均已布局自动…