和无用代码说再见!阿里文娱无损代码覆盖率统计方案
zhezhongyun 2025-05-14 18:25 1 浏览
作者 | 阿里巴巴文娱高级无线开发工程师 孙珑达
责编 | 屠敏
背景
为了适应产品的快速迭代,通常大量的研发资源会投入在新功能的开发上,而针对无用功能的治理却很少被关注。随着时间的推移,线上应用会积累大量的无用代码,再加上人员更迭以及功能交接,治理无用代码的成本越来越高。最终应用安装包过大,导致应用下载转化率降低、应用平台上架受限(例如超过100M的应用不能上架谷歌商店)、研发效率降低等等。
如何治理无用代码?首先是代码静态扫描。对于Android应用,ProGuard工具可以在构建阶段静态分析代码引用关系,自动裁减掉没有被引用到的代码,减少安装包大小。
当然,只有代码静态扫描是不够的,因为它不能代表线上用户实际的使用情况,所以还需要一套线上用户代码覆盖率的统计方案。
下面我将从Android应用的线上代码覆盖率统计切入,分享优酷的无用代码治理的技术思考和落地方案。
传统采集方案
首先,在需要统计的代码处加上统计代码。当代码被执行时,进行统计和上报。应用的代码行数通常都数以万计,手动添加显然是不现实的,所以一般会在构建阶段通过面向切面编程(AOP)来插入统计代码(以下简称为插桩),可以借助一些成熟的AOP中间件完成,例如,Jacoco、ASM。
其次,需要思考是,我们期望采集的粒度是什么?一般来说,粒度从细到粗分为:指令、分支、方法、类级别,粒度越细,代码覆盖率结果越准确,但性能损耗也越大。例如,如果想采集的粒度为指令级别,就需要对每个指令进行插桩,但这种插装会导致指令数也翻倍,安装包增大并且运行时性能下降。
优酷曾尝试过用Jacoco进行分支粒度的插桩,当时希望覆盖尽量多的用户,因为覆盖的用户越多结果越准确。但经测试,此方案使安装包增大10M,运行时性能严重恶化,果断放弃了此方案。
为了权衡性能和采集粒度,目前我们一般都采取类级别粒度的插桩,一方面是因为这样对性能影响较小,另一方面过细的采集粒度反而会加重业务方治理的难度。但此方案还不够完美:
1)运行时性能:当类首次加载时会执行统计代码,App启动过程会加载成千上万个类 ,会对启动性能造成一定影响;
2)包大小:有多少个类,就会插入多少行统计代码,对于像优酷这种大型App,也会增加不少的安装包大小;
3)构建耗时:因为构建过程中需要对每个类进行插桩,增加了构建耗时;
新采集方案—SlimLady
目标
优酷希望有一套方案可以无损地采集线上代码覆盖率,核心目标如下:
运行时性能:无任何影响;
包大小:无任何影响;
构建耗时:无任何影响;
实现
通过研究源码,发现可以通过动态查询DVM虚拟机已加载类的信息来获取类级别的代码覆盖率,下图中“覆盖率采集”部分即为SlimLady采集的原理图,这里我们只关注这部分,其他部分将在后面的整体方案中进行讲解。
ClassTable
Java虚拟机规范规定,类在使用前要先被虚拟机加载。Android中,是通过ClassLoader来完成类加载的,最后保存在Native层的ClassTable中,所以如果我们获取了所有ClassLoader的ClassTable对象,就有可能判断出虚拟机加载了哪些类。
首先,获取所有的ClassLoader对象。对于APK中的类,如果无特殊声明,一般都会被默认的PathClassLoader加载;对于动态加载的类,需要在自定义的ClassLoader中加载,例如Atlas会为每个Bundle创建一个相应的ClassLoader,通过这个ClassLoader来加载Bundle中的类。一旦明确了App中用到了哪些ClassLoder,获取是易如反掌的
其次,通过ClassLoader来获取ClassTable的对象的地址。通过Java层ClassLoader类的源码可知,ClassLoader有一个成员变量classTable(7.0及以上版本),这个变量保存了Native层ClassTable对象的地址,我们可以通过反射获取这个地址:
ClassLoader classLoader = XXX;
Field classTableField = ClassLoader.class.getDeclaredField("classTable");
classTableField.setAccessible(true);
long classTableAddr = classTableField.getLong(classLoader);
但在9.0的系统中成员变量classTable被加入了深灰名单,限制了直接反射,需要通过系统类进行反射绕过此限制:
ClassLoader classLoader = XXX;
Method metaGetDeclaredField = Class.class.getDeclaredMethod("getDeclaredField", String.class);
Field classTableField = (Field) metaGetDeclaredField.invoke(ClassLoader.class, "classTable");
classTableField.setAccessible(true);
long classTableAddr = classTableField.getLong(classLoader);
至此,我们就获得了所有的ClassTable对象的地址,里面保存了全部的类加载信息。
类名列表
通过阅读源码发现ClassTable有个方法可以通过类名查询类是否被加载过(下节将详细介绍),这样我们只需要获得所有类名的列表,再调用那个方法,即可以判断类是否被加载过。
APK中的类名列表可以通过DexFile进行获取,如下:
List<String> classes = new ArrayList<>;
DexFile df = new DexFile(context.getPackageCodePath);
for (Enumeration<String> iter = df.entries; iter.hasMoreElements; ) {
classes.add(iter.nextElement);
}
同理,动态加载的类也可以通过DexFile获取;
类是否被加载
通过阅读源码发现class_table.cc中,ClassTable有个Lookup方法,传入类名和类名的hash值,返回类对象的地址,如下:
mirror::Class* ClassTable::Lookup(const char* descriptor, size_t hash)
如果返回值为ptr,说明没有加载过此类,否则,说明加载过。
mirror::Class* ClassTable::Lookup(const char* descriptor, size_t hash)
获取此方法地址的方法:
加载so:class_table.cc在libart.so中,所有我们需要用dlopen加载libart.so获得此so的handler。其实在加载前,libart.so在当前进程一定已经被加载过了,此次加载只是为了获得handler,并不耗时;
符号表:通过readelf查询Lookup的符号:
_ZN3art10ClassTable6LookupEPKcj;方法指针:调用dlsym,传入handler和符号表,即可以找到Lookup方法的地址;
注:从7.0系统开始,Google禁止了调用系统Native的API,这里我们通过/proc/self/maps找到libart.so的地址,将里面的符号表进行拷贝,进而绕过此限制;
至此,我们就可以通过调用ClassTable的Lookup方法,传入类名和hash值,判断类是否被加载过了。
总结
这样,我们就能知道某一时刻有哪些类被加载过,对其上传,进行聚合和处理,再通过对比所有类名列表,就能得到代码覆盖率数据了。此方案不需要插桩,所以可以无损地采集覆盖率。
新方案整体设计
上面提到的采集方案是整个方案的核心,除此之外还有上下游的配套流程,整体方案的设计如下图:
1)APK分发:通过构建中心构建出最新的APK,分发给用户;
2)触发采集:用户安装应用,在使用过程中,将APP退后台10s后,通过采样率计算是否命中,若命中,则触发代码覆盖率采集
3)配置下发:在需要时,可以通过配置中心下发配置来动态调整功能开关、采样率等配置;
4)数据采集:代码覆盖率采集中间件(SlimLady)统计出被加载的类,将已加载的类名保存在文件中,进行压缩,将压缩后的数据传给上传中间件;
5)数据上传:上传中间件将数据上传到云端;
6)数据下载:服务器定期对云端数据进行下载;
7)类信息提供:服务器从构建中心获取类信息,包括所有类名列表和混淆文件;
8)数据解析:服务器按版本对代码覆盖率数据进行解压、反混淆、聚合统计,聚合统计后的结果包含了被加载过的类及次数,与所有类名列表进行对比,即可以知道哪些类没有被加载过,将结果保存至数据库;
9)结果聚合:网页端从数据库读取聚合结果,按模块展示代码覆盖率、模块热度、模块大小等信息。
总结
本方案突破了传统的插桩埋点统计,动态获取虚拟机信息,无损地采集代码覆盖率。有了代码覆盖率数据,能做的治理有很多,例如:下线无用代码、模块;瘦身或下线调用低频、体积大的模块;在集成阶段添加代码覆盖率卡口等等。
- 上一篇:Java 反射与注解详解
- 下一篇:C#通过反射给对象动态添加属性的实现
相关推荐
- Axure教程
-
来人人都是产品经理【起点学院】,BAT实战派产品总监手把手系统带你学产品、学运营。前几天看到有人介绍了axure8的图片放大原型设计步骤(http://www.woshipm.com/rp/24687...
- 保姆级教程:手把手教你用 Readdy.ai 轻松产品设计与前端开发
-
本文将为你介绍一款强大的AI工具——Readdy.ai,它不仅能自动生成精美的UI设计稿,还能直接生成可用的前端代码,极大地提升了开发效率。前几天雷神为大家介绍了一款AI设计工具莫高设计,但是...
- 前端学习保姆级教程,轻松入门 Web 开发
-
在当今数字化时代,我们每天浏览的网页、使用的各类应用,其美观的界面、流畅的交互体验背后,都离不开前端开发技术的支撑。前端开发,简单来说,就是负责将网站或应用的界面呈现给用户,它直接影响着用户对产品的第...
- 分享几个css实用技巧
-
本篇将介绍几个css小技巧,目录如下:自定义引用标签的符号重置所有标签样式禁止文本选择制作小三角形自定义<q>引用标签的符号默认q标签引用符号是浏览器根据不同语言环境自动设置的,当然我们也...
- 五步做一个高保真可交互原型-Principle教程
-
这篇教程要介绍的案例是:App界面上调整时间的可交互原型。我们先看一下效果:这个案例用的是Principle这款软件做的。Principle近段时间非常流行的一款做原型的软件。很多公司也用Princi...
- asp网站源码安装教程
-
asp网站源码安装指南1、打开控制面板并单击2、点选管理工具选项3、请单击打开Internet信息服务(IIS)管理器应用查看4、点击开始按钮5、请将项目文件移动到C:inetpubwwwr...
- web前端HTML教程——开发环境搭建下载和安装编辑器
-
html开发环境搭建有一个好的编辑器我们可以方便地的开发项目,编写代码,配置和管理我们的项目。所以我们开始编写html代码之前需要搭建开发环境。基于html项目的开发和代码编写现在网上有很多编辑器,也...
- 如何用控制台打印日历?
-
朋友们大家好,今天我给大家带来一个控制台小程序——打印当月的日历,效果如下图:笔者运行这个程序的时候是2022年7月30日,所以在日期的30后面加了一个*符号。这个程序很简单,但有些知识点我也会为大家...
- dotnet 值拷贝、浅拷贝、深拷贝详解
-
简介在.NET中,值拷贝(ValueCopy)主要指的是将一个值类型的实例或对象的值复制到另一个变量中,使两个变量之间互不影响。我们可以从几个维度来详细理解:值拷贝的本质.NET中的类型分...
- 41 基于Sentinel的限流
-
Sentinel支持对SpringCloudGateway、Zuul等主流的APIGateway进行限流。从1.6.0版本开始,Sentinel提供了SpringCloud...
- Java反射机制:神秘之门的钥匙
-
Java反射机制:神秘之门的钥匙什么是Java反射?当你第一次听说Java反射的时候,你可能会感到困惑。简单来说,Java反射就是让你在程序运行时动态地操作类和对象的能力。这就像一把钥匙,能够打开Ja...
- Java反射机制:魔法般的代码操控
-
Java反射机制:魔法般的代码操控在这个充满无限可能的编程世界里,Java反射机制就像一位神秘的魔法师,它允许程序在运行时检查类、方法、字段等信息,甚至还能动态调用这些成员。这种能力让Java开发者能...
- 自己动手从0开始实现一个分布式 RPC 框架
-
前言为什么要自己写一个RPC框架,我觉得从个人成长上说,如果一个程序员能清楚的了解RPC框架所具备的要素,掌握RPC框架中涉及的服务注册发现、负载均衡、序列化协议、RPC通信协议、Socket通信、异...
- C# 实战指南:使用 iText7 高效处理 PDF 文件
-
1.引言在现代开发中,处理PDF文件是一个常见的需求,例如生成报告、填充表单、设置权限或签名等。iText7是一个功能强大的PDF操作库,支持多种PDF操作,包括表单填充、加密、数字签...
- 手写代码生成工具实现类似Mybatis-Plus的效果-----02
-
#全新智己L6全国试驾开启#packagecom.alatus.builder;importcom.alatus.Entity.FieldInfo;importcom.alatus.Ent...
- 一周热门
- 最近发表
- 标签列表
-
- HTML 教程 (33)
- HTML 简介 (35)
- HTML 实例/测验 (32)
- HTML 测验 (32)
- HTML 参考手册 (28)
- JavaScript 和 HTML DOM 参考手册 (32)
- HTML 拓展阅读 (30)
- HTML常用标签 (29)
- HTML文本框样式 (31)
- HTML滚动条样式 (34)
- HTML5 浏览器支持 (33)
- HTML5 新元素 (33)
- HTML5 WebSocket (30)
- HTML5 代码规范 (32)
- HTML5 标签 (717)
- HTML5 标签 (已废弃) (75)
- HTML5电子书 (32)
- HTML5开发工具 (34)
- HTML5小游戏源码 (34)
- HTML5模板下载 (30)
- HTTP 状态消息 (33)
- HTTP 方法:GET 对比 POST (33)
- 键盘快捷键 (35)
- 标签 (226)
- HTML button formtarget 属性 (30)