摘要:\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E我们逐个来分析这几个方案:\u003C\u002Fp\u003E\u003Cp\u003E\u003Cstrong\u003E1. Gradle 依赖树\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cp\u003E使用 .\u002Fgradlew ::dependencies --configuration releaseCompileClasspath -q 命令,很容易就可以得到模块的依赖树,如图:\u003C\u002Fp\u003E\u003Cdiv class=\"pgc-img\"\u003E\u003Cimg src=\"http:\u002F\u002Fp9.pstatp.com\u002Flarge\u002Fpgc-image\u002F83d306c9abbf4f9599b976ea036f2c91\" img_width=\"932\" img_height=\"254\" alt=\"字节码技术在模块依赖分析中的应用\" inline=\"0\"\u003E\u003Cp class=\"pgc-img-caption\"\u003E\u003C\u002Fp\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E不难发现,这种方式有两个问题:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E声明即依赖,即使代码中没有使用的库,也会输出到结果中。如图:\u003C\u002Fp\u003E\u003Cdiv class=\"pgc-img\"\u003E\u003Cimg src=\"http:\u002F\u002Fp9.pstatp.com\u002Flarge\u002Fpgc-image\u002Ffaed2da4c66a4815b2faf8b7bddd44f0\" img_width=\"932\" img_height=\"336\" alt=\"字节码技术在模块依赖分析中的应用\" inline=\"0\"\u003E\u003Cp class=\"pgc-img-caption\"\u003E\u003C\u002Fp\u003E\u003C\u002Fdiv\u003E\u003Cp\u003EAndroid Studio 能准确分析到模块之间“方法级别”的引用关系,支持在 IDE 中跳转查看,也能扫描到对 Android SDK 的引用。

"\u003Cdiv\u003E\u003Ch1\u003E\u003Cstrong\u003E背景\u003C\u002Fstrong\u003E\u003C\u002Fh1\u003E\u003Cp\u003E近年来,随着手机业务的快速发展,为满足手机端用户诉求和业务功能的迅速增长,移动端的技术架构也从单一的大工程应用,逐步向模块化、组件化方向发展。以高德地图为例,Android 端的代码已突破百万行级别,超过100个模块参与最终构建。\u003C\u002Fp\u003E\u003Cp\u003E试想一下,如果没有一套标准的依赖检测和监控工具,用不了多久,模块的依赖关系就可能会乱成一锅粥。\u003C\u002Fp\u003E\u003Cp\u003E从模块 Owner 的角度看,为什么依赖分析这么重要?\u003C\u002Fp\u003E\u003Cp\u003E1.作为模块 Owner,我首先想知道“谁依赖了我?依赖了哪些接口”。唯有如此才能评估本模块改动的影响范围,以及暴露的接口的合理性。\u003C\u002Fp\u003E\u003Cp\u003E2.我还想知道“我依赖了谁?调用了哪些外部接口”,对所需要的外部能力做到心中有数。\u003C\u002Fp\u003E\u003Cp\u003E从全局视角看,一个健康的依赖结构,要防止“下层模块”直接依赖“上层模块”,更要杜绝循环依赖。通过分析全局的依赖关系,可以快速定位不合理的依赖,提前暴露业务问题。\u003C\u002Fp\u003E\u003Cp\u003E因此,依赖分析是研发过程中非常重要的一环。\u003C\u002Fp\u003E\u003Ch1\u003E\u003Cstrong\u003E常见的依赖分析方式\u003C\u002Fstrong\u003E\u003C\u002Fh1\u003E\u003Cp\u003E提到 Android 依赖分析,首先浮现在脑海中的可能是以下这些方案:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E分析 Gradle 依赖树。\u003C\u002Fli\u003E\u003Cli\u003E扫描代码中的 import 声明。\u003C\u002Fli\u003E\u003Cli\u003E使用 Android Studio 自带的分析功能。\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E我们逐个来分析这几个方案:\u003C\u002Fp\u003E\u003Cp\u003E\u003Cstrong\u003E1. Gradle 依赖树\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cp\u003E使用 .\u002Fgradlew :<module>:dependencies --configuration releaseCompileClasspath -q 命令,很容易就可以得到模块的依赖树,如图:\u003C\u002Fp\u003E\u003Cdiv class=\"pgc-img\"\u003E\u003Cimg src=\"http:\u002F\u002Fp9.pstatp.com\u002Flarge\u002Fpgc-image\u002F83d306c9abbf4f9599b976ea036f2c91\" img_width=\"932\" img_height=\"254\" alt=\"字节码技术在模块依赖分析中的应用\" inline=\"0\"\u003E\u003Cp class=\"pgc-img-caption\"\u003E\u003C\u002Fp\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E不难发现,这种方式有两个问题:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E声明即依赖,即使代码中没有使用的库,也会输出到结果中。\u003C\u002Fli\u003E\u003Cli\u003E只能分析到模块级别,无法精确到方法级别。\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E\u003Cstrong\u003E2. 扫描 import 声明\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cp\u003E扫描 Java 文件中的 import 语句,可以得到文件(类)之间的调用关系。\u003C\u002Fp\u003E\u003Cp\u003E因为模块与文件(类)的对应关系非常容易得到(扫描目录)。所以,得到了文件(类)之间的依赖关系,即是得到了模块之间文件(类)级别的依赖关系。\u003C\u002Fp\u003E\u003Cp\u003E这个方案相比 Gradle 依赖扫描提升了结果维度,可以分析到文件(类)级别。但是它也存在一些缺点:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E无法处理 import * 的情况。\u003C\u002Fli\u003E\u003Cli\u003E扫描“有 import 但未使用对应类”的场景效率太低(需要做源码字符串查找)。\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E\u003Cstrong\u003E3. 使用 IDE 自带的分析功能\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cp\u003E触发 Android Studio 菜单 「Analyze」 -> 「Analyze Dependencies」,可以得到模块间方法级别的依赖关系数据。如图:\u003C\u002Fp\u003E\u003Cdiv class=\"pgc-img\"\u003E\u003Cimg src=\"http:\u002F\u002Fp9.pstatp.com\u002Flarge\u002Fpgc-image\u002Ffaed2da4c66a4815b2faf8b7bddd44f0\" img_width=\"932\" img_height=\"336\" alt=\"字节码技术在模块依赖分析中的应用\" inline=\"0\"\u003E\u003Cp class=\"pgc-img-caption\"\u003E\u003C\u002Fp\u003E\u003C\u002Fdiv\u003E\u003Cp\u003EAndroid Studio 能准确分析到模块之间“方法级别”的引用关系,支持在 IDE 中跳转查看,也能扫描到对 Android SDK 的引用。\u003C\u002Fp\u003E\u003Cp\u003E这个方案比前面两个都优秀,主要是准确。但是它也有几个问题:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E耗时较长:全面分析 AMap 全源码,大约需要 10 分钟。\u003C\u002Fli\u003E\u003Cli\u003E分析结果无法为第三方复用,无法生成可视化的依赖关系图。\u003C\u002Fli\u003E\u003Cli\u003E分析正向依赖和逆向依赖,需要扫描两次。\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E总结一下上述三种方案:Gralde 依赖基于工程配置,粒度太粗且结果不准。“Import 扫描方案”能拿到文件级别依赖但数据不全。IDE 扫描虽然结果精准,但是数据复用困难,不便于工程化。\u003C\u002Fp\u003E\u003Ch1\u003E\u003Cstrong\u003E为什么要使用字节码来分析?\u003C\u002Fstrong\u003E\u003C\u002Fh1\u003E\u003Cdiv class=\"pgc-img\"\u003E\u003Cimg src=\"http:\u002F\u002Fp1.pstatp.com\u002Flarge\u002Fpgc-image\u002F6b48b0f270a64883a3356a89962f9853\" img_width=\"1042\" img_height=\"1164\" alt=\"字节码技术在模块依赖分析中的应用\" inline=\"0\"\u003E\u003Cp class=\"pgc-img-caption\"\u003E\u003C\u002Fp\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E参考 Android 构建流程图,所有的 Java 源代码和 aapt 生成的 R.java 文件,都会被编译成 .class 文件,再被编译为 dex 文件,最终通过 apkbuilder 生成到 apk 文件中。图中的 .class 文件即是我们所说的 Java 字节码,它是对 Java 源码的二进制转义。\u003C\u002Fp\u003E\u003Cp\u003E在 Android 端,常见的字节码应用场景包括:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E字节码插桩:用于实现对 UI 、内存、网络等模块的性能监控。\u003C\u002Fli\u003E\u003Cli\u003E修改 jar 包:针对无源码的库,通过编辑字节码来实现一些简单的逻辑修改。\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E回到本文的主题,为什么要分析字节码,而不是 Java 代码或者 dex 文件?\u003C\u002Fp\u003E\u003Cp\u003E不使用 Java 代码是因为有些库以 jar 或者 aar 的方式提供,我们获取不到源码。不使用 dex 文件是因为它没有好用的语法分析工具。所以解析字节码几乎是我们唯一的选择。\u003C\u002Fp\u003E\u003Ch1\u003E\u003Cstrong\u003E如何使用字节码分析依赖关系?\u003C\u002Fstrong\u003E\u003C\u002Fh1\u003E\u003Cp\u003E要得到模块之间的依赖关系,其实就是要得到“模块间类与类”之间的依赖关系。而要确定类之间的关系,分析类字节码的语句即可。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cstrong\u003E1. 在什么时机来分析?\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cp\u003E了解 Android 构建流程的同学,应该对 transform 这个任务不陌生。它是 Android Gradle 插件提供的一个字节码 Hook 入口。\u003C\u002Fp\u003E\u003Cp\u003E在 transform 这个任务中,所有的字节码文件(包括三方库) 以 Input 的格式输入。\u003C\u002Fp\u003E\u003Cp\u003E以JarInput 为例,分析其 file 字段,可得到模块的名称。解析 file 文件,即可得到此模块所有的字节码文件。\u003C\u002Fp\u003E\u003Cdiv class=\"pgc-img\"\u003E\u003Cimg src=\"http:\u002F\u002Fp1.pstatp.com\u002Flarge\u002Fpgc-image\u002Ff66b0691eaa546518763b144241edc26\" img_width=\"1616\" img_height=\"238\" alt=\"字节码技术在模块依赖分析中的应用\" inline=\"0\"\u003E\u003Cp class=\"pgc-img-caption\"\u003E\u003C\u002Fp\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E有了模块名称和对应路径下的 class 文件,就建立了模块与类的对应关系,这是我们拿到的第一个关键数据。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cstrong\u003E2. 使用什么工具分析?\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cp\u003E解析 Java 字节码的工具,最常用的包括 Javassit,ASM,CGLib。ASM 是一个轻量级的类库,性能较好,但需要直接操作 JVM 指令。CGLib 是对 ASM 的封装,提供了更高级的接口。\u003C\u002Fp\u003E\u003Cp\u003E相比而言,Javassist 要简单的多,它基于 Java 的 API ,无需操作 JVM 指令,但其性能要差一些(因为 Javassit 增加了一层抽象)。在工程原型阶段,为了快速验证结果,我们优先选择了 Javassit 。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cstrong\u003E3. 具体方案是怎样的?\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cp\u003E先看一个简单的示例,如何分析下面这段代码的调用关系:\u003C\u002Fp\u003E\u003Cpre\u003E1: package com.account;\u003Cbr\u003E2: import com.account.B;\u003Cbr\u003E3: public class A {\u003Cbr\u003E4: void methodA() {\u003Cbr\u003E5: B b = new B(); \u002F\u002F 初始化了 Class B 的实例 b\u003Cbr\u003E6: b.methodB(); \u002F\u002F 调用了 b 的 methodB 方法\u003Cbr\u003E7: }\u003Cbr\u003E8: }\u003Cbr\u003E\u003C\u002Fpre\u003E\u003Cp\u003E第1步:初始化环境,加载字节码 A.class,注册语句分析器。\u003C\u002Fp\u003E\u003Cpre\u003E\u002F\u002F 初始化 ClassPool,将字节码文件目录注册到 Pool 中。\u003Cbr\u003EClassPool pool = ClassPool.getDefault();\u003Cbr\u003Epool.insertClassPath('<class文件所在目录>')\u003Cbr\u003E\u002F\u002F 加载类A\u003Cbr\u003ECtClass cls = pool.get(\"com.account.A\");\u003Cbr\u003E\u002F\u002F 注册表达式分析器到类A\u003Cbr\u003EMyExprEditor editor = new MyExprEditor(ctCls)\u003Cbr\u003EctCls.instrument(editor)\u003Cbr\u003E\u003C\u002Fpre\u003E\u003Cp\u003E第2步:自定义表达式解析器,分析类A(以解析语句调用为例)。\u003C\u002Fp\u003E\u003Cpre\u003Eclass MyExprEditor extends ExprEditor {\u003Cbr\u003E@Override\u003Cbr\u003Evoid edit(MethodCall m) {\u003Cbr\u003E \u002F\u002F 语句所在类的名称\u003Cbr\u003E def clsAName = ctCls.name\u003Cbr\u003E \u002F\u002F 语句在哪个方法被调用\u003Cbr\u003E def where = m.where().methodInfo.getName()\u003Cbr\u003E \u002F\u002F 语句在哪一行被调用\u003Cbr\u003E def line = m.lineNumber\u003Cbr\u003E \u002F\u002F 被调用类的名称\u003Cbr\u003E def clsBName = m.className\u003Cbr\u003E \u002F\u002F 被调用的方法\u003Cbr\u003E def methodBName = m.methodName\u003Cbr\u003E}\u003Cbr\u003E\u002F\u002F 省略其它解析函数 ...\u003Cbr\u003E}\u003Cbr\u003E\u003C\u002Fpre\u003E\u003Cp\u003EExprEditor 的 edit(MethodCall m) 回调能拦截 Class A 中所有的方法调用(MethodCall)。\u003C\u002Fp\u003E\u003Cp\u003E除了本例中对 MethodCall 的解析,它还支持解析 new,new Array,ConstructorCall,FieldAccess,InstanceOf,强制类型转换,try-catch 语句。\u003C\u002Fp\u003E\u003Cp\u003E解析完 Class A,我们得到了 A 对 B 的依赖信息 :\u003C\u002Fp\u003E\u003Cdiv class=\"pgc-img\"\u003E\u003Cimg src=\"http:\u002F\u002Fp1.pstatp.com\u002Flarge\u002Fpgc-image\u002Fee0400a08fc34aa7b1134593d1f3f09e\" img_width=\"895\" img_height=\"181\" alt=\"字节码技术在模块依赖分析中的应用\" inline=\"0\"\u003E\u003Cp class=\"pgc-img-caption\"\u003E\u003C\u002Fp\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E简单解释如下:\u003C\u002Fp\u003E\u003Cp\u003E类 com.account.A 的第5行(methodA方法内),调用了 com.account.B 的构造函数;\u003C\u002Fp\u003E\u003Cp\u003E类 com.account.A 的第6行(methodA方法内),调用了 com.account.B 的 methodB 函数;\u003C\u002Fp\u003E\u003Cp\u003E这便是“类和类之间方法级”的依赖数据。结合第1步得到的“模块和类”的对应关系,最终我们便获得了“模块间方法级的依赖数据”。\u003C\u002Fp\u003E\u003Cp\u003E基于这些基础数据,我们还可以自定义依赖检测规则、生成全局的模块依赖关系图等,本文就不展开了。\u003C\u002Fp\u003E\u003Ch1\u003E\u003Cstrong\u003E小结\u003C\u002Fstrong\u003E\u003C\u002Fh1\u003E\u003Cp\u003E本文主要介绍了模块依赖分析在研发过程中的重要性,分析了 Android 常见的依赖分析方案,从 Gradle 依赖树分析, Import 扫描,使用 IDE 分析,到最后的字节码解析,方案逐步递进。越是接近源头的解法,才是越根本的解法。\u003C\u002Fp\u003E\u003Cp\u003E本文作者:高德技术小哥\u003C\u002Fp\u003E\u003C\u002Fdiv\u003E"'.slice(6, -6), groupId: '6719288015979545099
相关文章