写点什么

字节码技术在模块依赖分析中的应用

  • 2020-03-04
  • 本文字数:3164 字

    阅读完需:约 10 分钟

字节码技术在模块依赖分析中的应用

本文介绍 Java 字节码技术在 Android 模块依赖分析中的应用。文中的“字节码”特指“Java 字节码”。

背景

近年来,随着手机业务的快速发展,为满足手机端用户诉求和业务功能的迅速增长,移动端的技术架构也从单一的大工程应用,逐步向模块化、组件化方向发展。以高德地图为例,Android 端的代码已突破百万行级别,超过 100 个模块参与最终构建。


试想一下,如果没有一套标准的依赖检测和监控工具,用不了多久,模块的依赖关系就可能会乱成一锅粥。


从模块 Owner 的角度看,为什么依赖分析这么重要?


  1. 作为模块 Owner,我首先想知道“谁依赖了我?依赖了哪些接口”。唯有如此才能评估本模块改动的影响范围,以及暴露的接口的合理性。

  2. 我还想知道“我依赖了谁?调用了哪些外部接口”,对所需要的外部能力做到心中有数。


从全局视角看,一个健康的依赖结构,要防止“下层模块”直接依赖“上层模块”,更要杜绝循环依赖。通过分析全局的依赖关系,可以快速定位不合理的依赖,提前暴露业务问题。


因此,依赖分析是研发过程中非常重要的一环。

常见的依赖分析方式

提到 Android 依赖分析,首先浮现在脑海中的可能是以下这些方案:


  • 分析 Gradle 依赖树。

  • 扫描代码中的 import 声明。

  • 使用 Android Studio 自带的分析功能。


我们逐个来分析这几个方案:


1. Gradle 依赖树


使用 ./gradlew :<module>:dependencies --configuration releaseCompileClasspath -q 命令,很容易就可以得到模块的依赖树,如图:



不难发现,这种方式有两个问题:


  • 声明即依赖,即使代码中没有使用的库,也会输出到结果中。

  • 只能分析到模块级别,无法精确到方法级别。


2. 扫描 import 声明


扫描 Java 文件中的 import 语句,可以得到文件(类)之间的调用关系。


因为模块与文件(类)的对应关系非常容易得到(扫描目录)。所以,得到了文件(类)之间的依赖关系,即是得到了模块之间文件(类)级别的依赖关系。


这个方案相比 Gradle 依赖扫描提升了结果维度,可以分析到文件(类)级别。但是它也存在一些缺点:


  • 无法处理 import * 的情况。

  • 扫描“有 import 但未使用对应类”的场景效率太低(需要做源码字符串查找)。


3. 使用 IDE 自带的分析功能


触发 Android Studio 菜单 「Analyze」 -> 「Analyze Dependencies」,可以得到模块间方法级别的依赖关系数据。如图:



Android Studio 能准确分析到模块之间“方法级别”的引用关系,支持在 IDE 中跳转查看,也能扫描到对 Android SDK 的引用。


这个方案比前面两个都优秀,主要是准确。但是它也有几个问题:


  • 耗时较长:全面分析 AMap 全源码,大约需要 10 分钟。

  • 分析结果无法为第三方复用,无法生成可视化的依赖关系图。

  • 分析正向依赖和逆向依赖,需要扫描两次。


总结一下上述三种方案:Gralde 依赖基于工程配置,粒度太粗且结果不准。“Import 扫描方案”能拿到文件级别依赖但数据不全。IDE 扫描虽然结果精准,但是数据复用困难,不便于工程化。

为什么要使用字节码来分析?


参考 Android 构建流程图,所有的 Java 源代码和 aapt 生成的 R.java 文件,都会被编译成 .class 文件,再被编译为 dex 文件,最终通过 apkbuilder 生成到 apk 文件中。图中的 .class 文件即是我们所说的 Java 字节码,它是对 Java 源码的二进制转义。


在 Android 端,常见的字节码应用场景包括:


  • 字节码插桩:用于实现对 UI 、内存、网络等模块的性能监控。

  • 修改 jar 包:针对无源码的库,通过编辑字节码来实现一些简单的逻辑修改。


回到本文的主题,为什么要分析字节码,而不是 Java 代码或者 dex 文件?


不使用 Java 代码是因为有些库以 jar 或者 aar 的方式提供,我们获取不到源码。不使用 dex 文件是因为它没有好用的语法分析工具。所以解析字节码几乎是我们唯一的选择。

如何使用字节码分析依赖关系?

要得到模块之间的依赖关系,其实就是要得到“模块间类与类”之间的依赖关系。而要确定类之间的关系,分析类字节码的语句即可。


1. 在什么时机来分析?


了解 Android 构建流程的同学,应该对 transform 这个任务不陌生。它是 Android Gradle 插件提供的一个字节码 Hook 入口。


在 transform 这个任务中,所有的字节码文件(包括三方库) 以 Input 的格式输入。


以 JarInput 为例,分析其 file 字段,可得到模块的名称。解析 file 文件,即可得到此模块所有的字节码文件。



有了模块名称和对应路径下的 class 文件,就建立了模块与类的对应关系,这是我们拿到的第一个关键数据。


2. 使用什么工具分析?


解析 Java 字节码的工具,最常用的包括 Javassit,ASM,CGLib。ASM 是一个轻量级的类库,性能较好,但需要直接操作 JVM 指令。CGLib 是对 ASM 的封装,提供了更高级的接口。


相比而言,Javassist 要简单的多,它基于 Java 的 API ,无需操作 JVM 指令,但其性能要差一些(因为 Javassit 增加了一层抽象)。在工程原型阶段,为了快速验证结果,我们优先选择了 Javassit 。


3. 具体方案是怎样的?


先看一个简单的示例,如何分析下面这段代码的调用关系:


1: package com.account;2: import com.account.B;3: public class A {4:     void methodA() {5:         B b = new B(); // 初始化了 Class B 的实例 b6:         b.methodB();   // 调用了 b 的 methodB 方法7:     }8: }
复制代码


第 1 步:初始化环境,加载字节码 A.class,注册语句分析器。


// 初始化 ClassPool,将字节码文件目录注册到 Pool 中。ClassPool pool = ClassPool.getDefault();pool.insertClassPath('<class文件所在目录>')// 加载类ACtClass cls = pool.get("com.account.A");// 注册表达式分析器到类AMyExprEditor editor = new MyExprEditor(ctCls)ctCls.instrument(editor)
复制代码


第 2 步:自定义表达式解析器,分析类 A(以解析语句调用为例)。


class MyExprEditor extends ExprEditor {    @Override    void edit(MethodCall m) {        // 语句所在类的名称        def clsAName = ctCls.name        // 语句在哪个方法被调用        def where = m.where().methodInfo.getName()        // 语句在哪一行被调用        def line = m.lineNumber        // 被调用类的名称        def clsBName = m.className        // 被调用的方法        def methodBName = m.methodName    }    // 省略其它解析函数 ...}
复制代码


ExprEditor 的 edit(MethodCall m) 回调能拦截 Class A 中所有的方法调用(MethodCall)。


除了本例中对 MethodCall 的解析,它还支持解析 new,new Array,ConstructorCall,FieldAccess,InstanceOf,强制类型转换,try-catch 语句。


解析完 Class A,我们得到了 A 对 B 的依赖信息 :


---------------------------------------------------------------------------| Class1        | Class2        | Expr       | method1 | method2 | lineNo || ------------- | ------------- | ---------- | ------- | ------- | ------ || com.account.A | com.account.B | NewExpr    | methodA | <init>  | 5      || com.account.A | com.account.B | methodCall | methodA | methodB | 6      |------------------- -------------------------------------------------------简单解释如下:类 com.account.A 的第5行(methodA方法内),调用了 com.account.B 的构造函数;类 com.account.A 的第6行(methodA方法内),调用了 com.account.B 的 methodB 函数;
复制代码


这便是“类和类之间方法级”的依赖数据。结合第 1 步得到的“模块和类”的对应关系,最终我们便获得了“模块间方法级的依赖数据”。


基于这些基础数据,我们还可以自定义依赖检测规则、生成全局的模块依赖关系图等,本文就不展开了。

小结

本文主要介绍了模块依赖分析在研发过程中的重要性,分析了 Android 常见的依赖分析方案,从 Gradle 依赖树分析, Import 扫描,使用 IDE 分析,到最后的字节码解析,方案逐步递进。越是接近源头的解法,才是越根本的解法。


2020-03-04 14:481452

评论

发布
暂无评论
发现更多内容

地狱开局的2022,穿好你的安全铠甲

脑极体

Go 中的空白标识符(下划线)

宇宙之一粟

Go 语言 3月月更

Nginx限速模块初探

喀拉峻

nginx

windows下C与C++执行cmd命令并实时获取输出

DS小龙哥

3月月更

ABAP 文件上/下载

Jasen Ye

upload abap download template GRAPHICS

云原生时代已来,计算机教育如何因「云」而变?

阿里云弹性计算

云原生 ECS 计算机教育

自动化知识图谱表示:从三元组到子图

第四范式开发者社区

人工智能 自动化 知识图谱

Python 的排序方法 sort 和 sorted 的区别

AlwaysBeta

Python

Kubernetes中API的不同版本, Alpha, Beta, Stable 都是什么?

工程师薛昭君

Kubernetes API

从多快好省到好快省多,您的项目管理走对了吗?

禅道项目管理

项目管理

企业如何挖掘知识“金矿”?这本白皮书讲得够透彻!

百度大脑

网络安全入门5天速成教程: WEB安全渗透攻防技术

网络安全学海

网络安全 安全 信息安全 渗透测试 WEB安全

从0到1落地电商小程序之微服务设计

晨亮

「架构实战营」

手把手教你从Apk中取出算法

奋飞安全

android 安全 java

数仓如何设置大小写不敏感函数

华为云开发者联盟

MySQL DWS GaussDB(DWS) 大小写不敏感函数 GUC参数

客户画像赋能百度推广生态实践

百度Geek说

前端 后端

项目管理标准化的武林秘籍

大智若愚

团队管理 项目管理 标准化 软技能 标准框架

恒源云(GpuShare)_租卡怎么选?看这一篇就够了!

恒源云

人工智能 GPU服务器

治理有精度,AI赋智加强城市精细化管理

百度大脑

【愚公系列】2022年03月 Docker容器 Kafka集群的搭建

愚公搬代码

3月月更

AI+遥感智能解译,赋能智慧城市规划革新

百度大脑

产品升级|1-2月合刊:多款重磅产品来袭

百度大脑

你的“数学潜意识”原来可以被唤醒!

博文视点Broadview

微服务架构下消息服务多通道设计思路

全象云低代码

微服务 低代码 后端开发 消息中间件 后端技术

浅谈信息熵在数字体验监控领域的应用

博睿数据

详细的网站定制步骤有哪些?

源字节1号

网站开发 软件定制

无监控不运维—浅述各种监控方案使用场景

穿过生命散发芬芳

3月月更

从HDFS的写入和读取中,我发现了点东西

华为云开发者联盟

hdfs HDFS写入 HDFS读取 文件读取

java培训如何用反射做简易 Spring IOC 容器

@零度

Java springloc

加快云原生技术转型, 智能调度登陆华为云DevOps: 增速,节源

华为云开发者联盟

软件 DevOps 代码托管 智能调度 华为云DevOps

实践丨SpringBoot整合Mybatis-Plus项目存在Mapper时报错

华为云开发者联盟

spring 容器 Spring Boot 测试 Mybatis-Plus

字节码技术在模块依赖分析中的应用_文化 & 方法_高德技术_InfoQ精选文章