和无用代码说再见!阿里文娱无损代码覆盖率统计方案

核心提示作者 | 阿里巴巴文娱高级无线开发工程师 孙珑达责编 | 屠敏背景为了适应产品的快速迭代,通常大量的研发资源会投入在新功能的开发上,而针对无用功能的治理却很少被关注。随着时间的推移,线上应用会积累大量的无用代码,再加上人员更迭以及功能交接,

作者|孙,阿里文娱高级无线开发工程师

编辑|屠敏

背景

为了适应产品的快速迭代,通常会投入大量的RD资源进行新功能的开发,而对无用功能的治理却很少有人关注。随着时间的推移,在线应用会积累大量无用代码,而且随着人员的变动和职能的交接,管理无用代码的成本越来越高。最终应用安装包过大,导致应用下载转化率降低,应用平台受限,研发效率降低等等。

如何管理没用的代码?首先,静态扫描代码。对于Android应用,ProGuard工具可以在构建阶段静态分析代码引用关系,自动裁剪掉未引用的代码,减少安装包的大小。

当然,仅仅静态扫描代码是不够的,因为它不能代表在线用户的实际使用情况,所以需要一个在线用户代码覆盖率的统计方案。

我将从安卓应用的在线代码覆盖率统计来分享优酷无用代码治理的技术思路和落地方案。

传统采购方案

首先,将统计代码添加到要统计的代码中。代码执行时,进行统计和报告。应用代码的行数通常是上万行,手工添加显然不现实。所以通常在构建阶段通过面向方面的编程来插入统计代码,这可以通过一些成熟的AOP中间件来完成,比如Jacoco和ASM。

其次,我们需要思考,我们期望收集的粒度是多少?一般来说,粒度由细到粗分为指令级、分支级、方法级和类级。粒度越细,代码覆盖结果越准确,但性能损失越大。例如,如果收集的粒度是指令级的,那么每个指令都需要被插装,但是这种插装会使指令的数量加倍,增加安装包并降低运行时性能。

优酷尝试用Jacoco做分支粒度。当时是希望覆盖尽可能多的用户,因为覆盖的用户越多,结果越准确。但是经过测试,这个方案增加了10M的安装包,运行时性能恶化严重,所以果断放弃这个方案。

为了权衡性能和集合粒度,目前一般采用类级粒度插装。一方面对业绩影响不大;另一方面,过于精细的采集粒度会加剧业务端治理的难度。但是这个方案并不完美:

1)运行时性能:第一次加载类时,会执行统计代码,App启动过程中会加载上千个类,对启动性能有一定影响;

2)包大小:有多少个类就插入多少行统计代码。对于优酷这样的大型App,安装包的大小也会增加很多;

3)施工耗时:由于施工过程中需要插入每节课,增加了施工时间;

一种新的收购方案——苗条女士

目标

优酷希望能有一套解决方案,无损采集线上代码覆盖率。核心目标如下:

运行时性能:没有影响;包装尺寸:无影响;施工耗时:无影响;实现

通过对源代码的研究,发现可以通过动态查询DVM虚拟机已加载类的信息来获得类级的代码覆盖率。下图中的“覆盖集合”部分是SlimLady集合的示意图。这里只关注这一部分,其他部分会在下面的总体方案中说明。

类别表

Java虚拟机规范规定,类在使用前应该由虚拟机加载。在Android中,类的加载是通过ClassLoader完成的,最后保存在Native层的ClassTable中,所以如果我们得到所有类加载器的ClassTable对象,就有可能确定哪些类是虚拟机加载的。

首先,获取所有的类加载器对象。对于APK中的类,除非特别说明,一般都是用默认的PathClassLoader加载的;对于动态加载的类,它们需要被加载到自定义的类加载器中。例如,Atlas会为每个Bundle创建一个对应的类加载器,并通过这个类加载器加载Bundle中的类。一旦你知道应用程序中使用了哪些类,就很容易得到它们。

其次,通过ClassLoader获取ClassTable对象的地址。根据Java层的ClassLoader类的源代码,ClassLoader有一个成员变量classTable,存放原生层的ClassTable对象的地址。我们可以通过反射得到这个地址:

ClassLoader classLoader = XXXfield class tablefield = class loader . class . getdeclaredfield;class table field . set accessible;long class table addr = class table field . get long;

但在9.0系统中,成员变量classTable被加入了深灰列表,限制了直接反映。它需要由系统类来反映,以绕过这一限制:

ClassLoader classLoader = XXXmethod metaGetDeclaredField = class . class . getdeclaredmethod;field class tablefield = metagetcdeclaredfield . invoke;class table field . set accessible;long class table addr = class table field . get long;

至此,我们已经获得了所有ClassTable对象的地址,它们保存了所有的类加载信息。

类名列表

通过阅读源代码,发现ClassTable有一个方法,可以通过类名查询一个类是否已经加载,这样我们只需要得到一个所有类名的列表,然后调用那个方法就可以确定一个类是否已经加载。

APK的类名列表可以通过DexFile获得,如下所示:

List classes = new ArrayListdex file df = new dex file);对于;iter.hasMoreElements){ classes . add);}

同样,动态加载的类也可以通过DexFile获得;

类加载了吗?

通过阅读源代码,发现在class_table.cc中,ClassTable有一个查找方法,传入类名和类名的哈希值,返回类对象的地址,如下:

镜像::Class* ClassTable::查找

如果返回值为nullptr,则该类尚未加载;否则,它已被加载。

镜像::Class* ClassTable::查找

获取此方法地址的方法:

加载so: class _ table.cc在libart.so中,所以我们需要用dlopen加载libart.so来获取这个so的处理程序。其实在加载之前,libart.so肯定已经在当前进程中加载了。这个加载只是为了获取handler,并不耗时;符号表:通过readelf查询Lookup的符号:_ Zn 3 art 10 class table 6 lookupekcj;方法指针:调用dlsym,传入handler和符号表,可以找到查找方法的地址;注意:从7.0系统开始,Google已经禁止调用系统原生的API。这里我们通过/proc/self/maps找到libart.so的地址,并将符号表复制到里面,从而绕过了这个限制;

此时,我们可以通过调用ClassTable的查找方法并传入类名和哈希值来判断该类是否已经加载。

摘要

这样我们就可以知道在某个时刻加载了哪些类,上传、聚合、处理它们,然后通过比较所有类名的列表就可以得到代码覆盖率数据。这种方案不需要插桩,覆盖率可以无损采集。

新方案的总体设计

上述收购方案是整个方案的核心,此外还有上下游的配套流程。整个方案的设计如下:

1)APK分发:通过楼宇中心搭建最新的APK,分发给用户;

2)触发采集:用户安装应用,使用时,应用从后台退10s后,用采样率计算是否命中,如果命中,则触发代码覆盖采集。

3)配置的分配:必要时,功能开关、采样率等的配置。可以通过配置中心分配配置进行动态调整;

4)数据采集:代码覆盖采集中间件对加载的类进行统计,将加载的类名保存在文件中,进行压缩,并将压缩后的数据发送给上传中间件;

5)数据上传:上传中间件上传数据到云端;

6)数据下载:服务器定时下载云端数据;

7)班级信息提供:服务器从楼中心获取班级信息,包括所有班级名单和混淆文件;

8)数据分析:服务器根据版本对代码覆盖数据进行解压缩、反混淆、聚合。聚集的统计结果包括加载的类和时间。通过与所有类名进行比较,可以知道哪些类没有加载,并将结果保存到数据库中;

9)结果聚合:网页从数据库中读取聚合结果,显示代码覆盖率、模块热度、模块大小等信息。按模块。

摘要

该方案突破了传统的插桩埋点统计,动态获取虚拟机信息,无损采集代码覆盖率。有了代码覆盖数据,可以做的处理有很多,比如:离线无用代码和模块;或者瘦身调用低频大音量模块;在集成阶段增加代码覆盖卡口等等。

【结束】

 
友情链接
鄂ICP备19019357号-22