高德Android高性能高稳定性代码覆盖率技术实践

前言

代码覆盖率(Code coverage)是软件测试中的一种度量方式,用于反映代码被测试的比例和程度。

在软件迭代过程中,除了应该关注测试过程中的代码覆盖率,用户使用过程中的代码覆盖率也是一个非常有价值的指标,同样不可忽视。因为伴随着业务扩展和功能更新,产生了大量过时和废弃的代码,这些代码或者很少甚至完全不再使用,或者“年久失修”,缺少维护,不仅对应用包体积有影响,还可能带来稳定性风险。此时,能够采集生产环境的代码覆盖率,了解线上代码的使用情况,为下线无用代码提供依据,就十分重要了。

目标

我们的目标很明确:根据云端配置,采集线上每个类的触达和使用频次,上传到云端,在平台进行处理,并提供查询和报表展示能力

如上图所示,我们期望代码覆盖率数据能在平台上进行查询和直观的展示,在需要时可以直接查看,为下线旧代码、资源调度和分配等提供决策依据,最终为用户提供更小的App安装包,更好的功能使用体验。

通过云控中心,我们可以控制是否启用覆盖率采集,也可以根据覆盖率(类使用频次)动态调整App中金刚位、线程等资源的调度分配策略。其中覆盖率采集方案是最为重要的一环,业界也有很多成熟的方案,但都有各自适合的场景,而我们的诉求是在尽量不影响用户使用和App运行的前提下,采集类粒度的代码使用覆盖率。使用的采集方案应该少Hack,实现简单,兼顾稳定性和性能,同时也不会侵入打包流程,带来包体积影响等,在经过深入探索后,我们自研出了一套完美满足这些要求的全新方案。

方案对比

下表为常见方案与自研方案的各项指标对比,绿色表示更优。

从表格中可以看出:

Jacoco方案

类似的还有Emma、Cobertura等,他们都通过插桩实现,可以支持所有版本所有粒度的采集,但是插桩带来了一定的包体积和性能影响,不适合线上大范围使用。

Hook PathClassLoader方案

实现简单,无源码侵入,且支持所有Android版本,但Hook PathClassLoader不仅带来了性能影响,甚至可能波及App稳定性。

Hack访问ClassTable方案

能够按需采集,对App性能几乎没有影响,但Hack可能带来兼容性问题,且实现较复杂。

自研方案

  • 性能优异,支持按需采集,无损App性能
  • 实现简单,未使用任何“黑科技”,稳定性和兼容性极好
  • 支持跨进程和插件采集

对比得知自研方案能更好的满足我们采集线上代码覆盖率的诉求,因为它不仅有着很好的稳定性,而且有着优异的性能,几乎不会对用户产生任何影响。那么它是如何做到高性能和高稳定性的呢?请看下文介绍。

方案介绍

原理

要采集类粒度的代码覆盖率,其实就是要知道在App运行过程中,加载和使用了哪些类。在Java应用中,这可以通过调用ClassLoader的findLoadedClass方法直接查询得到,而在Android App中却没那么简单。原因是Android系统做了这样一个优化:

为了提升启动性能,对于App自定义的类,即PathClassLoader加载的类,如果直接调用findLoadedClass进行查询,即使这个类没有加载,也会执行加载操作。

这不是我们期望的。

虽然我们没办法直接调用FindLoadedClass方法查询类的加载状态,但是经过深入研究和分析,我们发现ClassLoader最终是通过查询它的ClassTable字段得到类加载状态的,如果我们也能访问ClassTable,问题不就迎刃而解了吗?沿着这个思路,我们创新性地提出了复制ClassTable指针,通过标准API间接访问类加载状态的方案。

该方案巧妙地实现了对ClassTable的无Hack访问;同时完美绕开了我们不需要的类加载优化,寥寥数行代码就实现了类加载情况的获取,巧妙且简洁,同时它还具备以下优势:

  • 采集速度是普通方案的5倍以上,性能优异
  • 使用标准API访问ClassTable,兼容性与稳定性极佳
  • 仅使用一次反射,无任何“黑科技”,简单稳定
  • 不影响类加载及App运行
  • 完美支持多进程和插件的采集

不过有一点需要注意:

ClassTable字段是从Android N开始引入的,所以该方法只适用于Android N及以上。出于必要性和ROI考虑,我们也未对Android N以下版本进行适配。

采集流程

基于上述的方案,我们设计了完整的代码覆盖率采集功能,关键流程如下:

可以看到整个端侧的采集流程是串行的,非常便于流程控制和数据整合。下面说明一下设计思路:

  • 采集时将App分为两部分,一部分是主进程和子进程使用的宿主类数据,另一部分是插件类数据。
  • 基于查询方式采集,主进程、子进程、插件分别提供查询类加载状态的接口。
  • 流程基于串行方式,由主进程控制,依次调用相应的接口采集主进程、子进程和插件的数据。
  • 每个版本只采集和上报未加载过的类数据,首次采集时,以类全集为输入;后续的每次采集,以上一版本未加载的类为输入,采集次数越多,需要查询的类越少。
  • 主进程和子进程依次查询,查询都以上一次查询后剩余的未加载类为输入,因此越靠后的子进程所需查询的数量越少,同一个插件在不同进程的实例的查询也与此类似。

如下图所示:

  • 采集结束时,会生成一份宿主类数据和N份插件类数据(假如有N个插件)。这些数据会分别与之前的采集结果做Diff,将增量数据上传服务。
  • 服务平台进行存储、解Mapping、模块关联等处理,最后以报表形式聚合展示。

值得注意的是:

  • 主进程与子进程使用的类都属于宿主,采集结果应该合并为一份数据;同理,一个插件无论在多少个进程加载,最后也只应生成一份该插件的数据。
  • 采集时我们将数据分为两部分,这样可以提高采集效率,也方便后续解混淆;在平台展示时,合并展示更有意义。

版本管理

Android App的代码大都会经过混淆处理,混淆后的类名会因版本而异,这就需要根据App版本来管理覆盖率数据。

按版本管理数据后,每个版本会清除上一版本的数据,避免数据错乱;一个特定的类,在当前版本已经使用过之后,会记录下来,后续此版本的采集不再重复查询它的使用情况。

每个版本首次采集时,需要以App的类名全集作为输入,每一次采集会产生一个未使用类的集合,作为下一次采集的输入。这样,一个版本中每次采集需要关注的类数量会逐步减少,可避免无意义的查询,提升采集性能。

类名数据获取

类名数据可以通过两种方式获取:

1.从安装包获取

安装包内的类名数据可以从PathClassLoader中获取,插件则可以从对应的BaseDexClassLoader中获取,使用如下方法即可:

public static List<String> getClassesFromClassLoader(BaseDexClassLoader classLoader) throws ClassNotFoundException, IllegalAccessException {//类名数据位于BaseDexClassLoader.pathList.dexElements.dexFile中,可以通过反射获取//先获取pathList字段Field pathListF = ReflectUtils.getField("pathList", BaseDexClassLoader.class);pathListF.setAccessible(true);Object pathList = pathListF.get(classLoader);//获取pathList中的dexElements字段Field dexElementsF = ReflectUtils.getField("dexElements", Class.forName("dalvik.system.DexPathList"));dexElementsF.setAccessible(true);Object[] array = (Object[]) dexElementsF.get(pathList);//获取dexElements中的dexFile字段Field dexFileF = ReflectUtils.getField("dexFile", Class.forName("dalvik.system.DexPathList$Element"));dexFileF.setAccessible(true);ArrayList<String> classes = new ArrayList<>(256);for (int i = 0; i < array.length; i++) {//获取dexFileDexFile dexFile = (DexFile) dexFileF.get(array[i]);//遍历DexFile获取类名数据Enumeration<String> enumeration = dexFile.entries();while (enumeration.hasMoreElements()) {classes.add(enumeration.nextElement());}}return classes;
}

这种方式简单直接,不过会一次性将DexFile中的所有类名加载到内存中,而根据我们的测试,每一万个类大约占0.8mb内存,对于动辄数万个类的大型App来说,会是一个不小的内存开销。所以还可以考虑第二种方式。

2.云化下载

从构建平台获取类名数据,上传到云化平台,App在需要的时候下载使用。

至于选用哪种方式,直接根据类数量来选取就好。类数量特别多时,如大型App场景,建议使用云化方式;普通App或插件,直接从安装包类获取即可。

子进程采集

主进程未加载的类,我们会交给子进程再次查询。这就需要子进程提供支持跨进程调用的查询接口,我们选择了简单可靠,且容易复用的AIDL方案来实现。

具体做法是:

通过AIDL定义查询接口,并定义对应的Action,在Service的onBind方法中根据Action返回查询接口的Binder实现类用于远程调用。

同时考虑到跨进程的成本较高,如果对每个类都调用一次查询接口,无疑是难以接受的。于是我们想到了文件+批量查询的方式:利用文件作为数据载体,将已加载的类和未加载的类都写入到文件中,在接口间传递文件路径。文件操作还可以采用BufferedReader和BufferedWriter以提升性能。

调用过程如图:

这样做的好处也显而易见:

  • 采集一个进程仅需一次跨进程调用,成本极低
  • 避免数据序列化的内存开销
  • 绕开大数据无法直接跨进程传递的问题
  • 采集流程更简单,可按需采集需要的进程
  • 方便数据过滤,避免重复查询已加载类,提升采集性能

插件采集

对于宿主类,查询PathClassLoader对应的ClassTable即可。

而插件一般通过BaseDexClassLoader或其派生类进行加载,需要查询相应ClassLoader的ClassTable。

对于在子进程中使用的插件,只是多了跨进程接口调用,将已加载类和剩余类返回给主进程进行处理的操作。

采集步骤如下:

  • 查询子进程类时,会同时查询该进程中运行的插件类,将数据写入按插件名划分的文件。
  • 对主进程插件的采集是整个流程的最后一个环节,此时会检测每个插件对应的数据文件(子进程生成),并进行合并处理,最后将数据文件删除。
  • 最后再处理剩余的插件数据文件,这部分文件属于只在子进程运行的插件。

到此,就得到了所有插件的类加载数据。

解Mapping

查看代码覆盖率数据时,我们期望看到原始的类名,所以解Mapping是必经之路。

解Mapping操作可以在端上进行,也可以在服务侧进行,出于安全性考虑,我们选择了服务侧。

Mapping文件由打包过程生成,每个安装包对应一份。我们的做法是在构建平台打正式包的时候通过脚本生成混淆类与明文类的映射文件,服务端在需要的时候通过App版本信息获取对应的映射文件,反解出原始类名,并与模块进行关联。

最终展示到平台的就是解完Mapping,并与模块、插件完成关联的代码覆盖率数据。

数据存储及增量计算

采集的数据需要存储起来,为了方便计算增量数据,我们选择了数据库作为存储方案,因为它天生具备去重及排序功能,而且性能也不错。具体的做法是:

  • 创建一张数据表,只需包含一个名为class的列就行,该列声明为主键,不接受空值和重复。
  • 每次采集前,获取其中的行数,采集过程中,将已加载的类名数据更新到表中,让数据库自动完成去重。采集完成后,再次获取数据行数,与采集前的行数相减得出的offset就是增量部分,我们只需要将这部分数据上传到服务。

性能和稳定性

经过我们的反复测试和调优,对5w+类的采集平均耗时约0.5s/次,采集期间内存增长在500kb左右,CPU无明显上涨。

同时也经过高德地图线上多个版本验证,未发现相关崩溃及ANR。

其他

绕开黑灰名单

Android P以后,官方将ClassTable成员变量加入了黑灰名单,在使用反射访问之前,需绕开SDK限制。我们采用的是元反射+设置豁免的方式,具体的实现可以参考GitHub上的开源项目FreeReflection,想要了解更多可自行Google查询。

采集时机和频率

虽然采集过程短暂无感,但为了最小的影响App的运行,我们将采集工作放在子线程中,并选择在App退后台一段时间后开始执行。

同时由于我们只需要知道代码使用的比例和大致情况,每次冷启后只采集一次即可。

多位用户多次冷启后的数据,已经足以反映真实的代码使用情况了。如果需要每个类的使用频次数据,在服务端聚合统计也能得到。

写在最后

代码覆盖率作为一种度量方式,不仅能为我们下线旧代码提供依据,同时还能反映某个功能的使用热度,可以为资源分配、调度决策等提供依据,是软件开发中一项不可或缺的重要工具。

我们这套全新的方案,简洁而不简单,巧妙地实现了无Hack采集,在保证高稳定性和不侵入源码的前提下,优雅地实现了生产环境代码覆盖率的高性能采集,已经过高德地图多版本验证,是一套成熟、稳定且高效的方案。在此分享出来,希望能为有同样诉求的同学提供一些借鉴和思路。

Android 学习笔录

Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

java八股文面试[java基础]——字节码的组成

什么是字节码&#xff1f; 因为JVM针对各种操作系统和平台都进行了定制&#xff0c;无论在什么平台&#xff0c;都可以通过javac命令将一个.java文件编译成固定格式的字节码&#xff08;.class文件&#xff09;供JVM使用。之所以被称为字节码&#xff0c;是因为.class文件是由…

用Qt自制一个小闹钟

小闹钟 功能 当按下启动按钮时&#xff0c;停止按钮可用&#xff0c;启动按钮不可用&#xff0c;闹钟无法设置&#xff0c;无法输入自定义内容 当按下停止按钮时&#xff0c;暂停播报&#xff0c;启动按钮可用&#xff0c;闹钟可以设置&#xff0c;可以输入自定义内容 .pro文…

如何在`Pycharm`中配置基于WSL的`Python Interpreters`,以及配置基于WSL的`Terminal`

文章目录 一、创建pycharm用户并授予sudo权限0. 启动WSL下的CentOS1. 创建pycharm用户并授予sudo权限2. 设置pycharm用户为wsl启动Linux的默认用户3. 重启并重新登录wsl下的CentOS4. 验证pycharm用户的sudo权限 二、创建基于WSL的Python Interpreter1. 添加基于WSL的Python Int…

微信小程序商城的定义、特点、优势、未来发展方向

微信小程序商城是一种新兴的电子商务模式&#xff0c;近年来风头火势&#xff0c;深受商家和消费者的喜爱。它是基于微信平台开发的一种小程序&#xff0c;用户可以通过微信进行搜索、购买商品或服务&#xff0c;实现线上购物的便利。本文将从微信小程序商城的定义、特点、优势…

【已解决】oracle获取最近2学年的数据

已解决 &#xff1a;oracle获取最近2学年的数据 SELECT * FROM (SELECT * FROM xx.JWXT_XSKB WHERE AND xn IN (‘2023-2024’,‘2022-2023’)); 问题 某某系统课表数据过大&#xff0c;要求只获取最近2学年的数据&#xff0c;不能写死。 思路 mysql 在子查询的WHERE子句中…

如何用PS把roughness贴图转换成Smoothness,并放入Metallic贴图的a通道。

1&#xff1a;用PS打开Roughness贴图 2&#xff1a;选择反相&#xff0c;装换成Smoothness贴图 3&#xff1a;新建一个大小相等的psd文件&#xff0c;或者打开Metallic贴图 4&#xff1a;如果没有金属度贴图&#xff0c;就把新建的图画成纯黑色 5&#xff1a;选择图层蒙版->…

PE文件格式详解

摘要 本文描述了Windows系统的PE文件格式。 PE文件格式简介 PE&#xff08;Portable Executable&#xff09;文件格式是一种Windows操作系统下的可执行文件格式。PE文件格式是由Microsoft基于COFF&#xff08;Common Object File Format&#xff09;格式所定义的&#xff0c…

android 实现本地一键打包,告别繁琐的studio操作

前言 在实际开发项目中&#xff0c;我们的工程目录往往是多个app在一个工程下的&#xff0c;每次打包都需要手动的用studio点击Build->Generate Signed Bundle or APK->APK 选择app&#xff0c;签名等&#xff0c;甚至有的app签名还不一样&#xff0c;还需要手动的来回切…

NC后端扩展开发

前言 在日常的工作中&#xff0c;会遇到各种各样的需要进行扩展开发的需求&#xff0c;可以使用系统预留的扩展开发机制来实现&#xff0c;避免修改源码。因NC产品已迭代至BIP版本&#xff0c;所以前端扩展方式就再进行不赘述了&#xff0c;本文主要介绍后端扩展开发方式&…

【FreeRTOS】【应用篇】消息队列【下篇】

前言 本篇文章主要对 FreeRTOS 中消息队列的概念和相关函数进行了详解消息队列【下篇】详细剖析了消息队列中发送、接收时队列消息控制块中各种指针的行为&#xff0c;以及几个发送消息和接收消息的函数的运作流程笔者有关于 【FreeRTOS】【应用篇】消息队列【上篇】——队列基…

redis 数据结构(二)

整数集合 整数集合是 Set 对象的底层实现之一。当一个 Set 对象只包含整数值元素&#xff0c;并且元素数量不时&#xff0c;就会使用整数集这个数据结构作为底层实现。 整数集合结构设计 整数集合本质上是一块连续内存空间&#xff0c;它的结构定义如下&#xff1a; typed…

nginx使用详解

文章目录 一、前言二、nginx使用详解2.1、nginx特点2.2 静态文件处理2.3 反向代理2.4 负载均衡2.5 高级用法2.5.1 正则表达式匹配2.5.2 重定向 三、总结 一、前言 本文将详细介绍nginx的各个功能使用&#xff0c;主要包括 二、nginx使用详解 2.1、nginx特点 高性能&#xff…

Unity中Shader的面剔除Cull

文章目录 前言一、Unity中Shader的面是否剔除&#xff0c;是由我们自己决定的二、暴露一个属性来控制 剔除模式三、如何区分正反面 前言 Unity中Shader的面剔除 Cull Off | Back | Front 一、Unity中Shader的面是否剔除&#xff0c;是由我们自己决定的 使用 Cull Off | Back |…

Pygame中Trivia游戏解析6-3

3.3 Trivia类的show_question()函数 Trivia类的show_question()函数的作用是显示题目。主要包括显示题目框架、显示题目内容和显示题目选项等三部分。 3.3.1 显示题目的框架 在show_question()函数中&#xff0c;通过以下代码显示题目的框架。 print_text(font1, 210, 5, &q…

【AI】数学基础——数理统计(概念参数估计)

概率论 文章目录 3.6 数理统计概念与定理3.6.1 概率论与数理统计区别3.6.2 基本定理大数定理马尔科夫不等式切比雪夫不等式中心极限定理 3.6.3 统计推断的基本问题 3.7 参数估计3.7.1 频率派点估计法矩阵估计法极大似然估计点估计量的评估 区间估计 3.7.2 贝叶斯派贝叶斯定理条…

Spring Boot 整合 Shiro(后端)

1 Shiro 什么是 Shiro 官网&#xff1a; http://shiro.apache.org/ 是一款主流的 Java 安全框架&#xff0c;不依赖任何容器&#xff0c;可以运行在 Java SE 和 Java EE 项目中&#xff0c;它的主要作用是对访问系统的用户进行身份认证、 授权、会话管理、加密等操作。 …

【ARM CoreLink CCI-400 控制器简介】

文章目录 CCI-400 介绍 CCI-400 介绍 CCI&#xff08;Cache Coherent Interconnect&#xff09;是ARM 中 的Cache一致性控制器。 CCI-400 将 Interconnect 和coherency 功能结合到一个模块中。它支持多达两个ACE master 点的interface&#xff0c;例如&#xff1a; Cortex-A…

[Mac软件]Adobe After Effects 2023 v23.5 中文苹果电脑版(支持M1)

After Effects是动画图形和视觉效果的行业标准。由运动设计师、平面设计师和视频编辑用于创建复杂的动画图形和视觉上吸引人的视频。 创建动画图形 使用预设样式为文本和图形添加动画效果&#xff0c;或逐帧调整它们。编辑、添加深度、制作动画或转换为可编辑的路径&#xff…

【Python】pytorch,CUDA是否可用,查看显卡显存剩余容量

CUDA可用&#xff0c;共有 1 个GPU设备可用。 当前使用的GPU设备索引&#xff1a;0 当前使用的GPU设备名称&#xff1a;NVIDIA T1000 GPU显存总量&#xff1a;4.00 GB 已使用的GPU显存&#xff1a;0.00 GB 剩余GPU显存&#xff1a;4.00 GB PyTorch版本&#xff1a;1.10.1cu102 …

【C++】C++面向对象编程三大特性之一——继承

❤️前言 本篇博客主要是关于C面向对象编程中的三大特性之一的继承&#xff0c;希望大家能和我一起共同学习进步&#xff01; 正文 我们刚刚学习一块全新的知识&#xff0c;首先简单关注一下它的概念和简单的使用方法。 继承的概念及定义 继承的概念 继承的概念&#xff1a;…