本文从一个小白的角度出发,介绍学习“字节码插桩”所需要了解掌握的知识,包括“字节码插桩”发挥作用的时机、应用场景、涉及的
.class
知识、ASM
框架、Gradle
编译知识等。
1. 什么是“字节码插桩”?
-
1.1 “字节码插桩”发生的时机
首先我们通过一张图,从宏观的角度认识“字节码插桩”。
AGP(
Android Gradle Plugin
)是Android
官方基于Gradle
开发的一些列编译配套插件,帮助开发者完成APP
的打包。上图是
Android
的编译过程,各类文件经过各种工具,最后变成.apk
文件。而“字节码插桩”就发生在
.class
文件变成.dex
文件之前。正是在这样的一个时机,“字节码插桩”才拥有修改全局.class
文件的能力。 -
1.2 “字节码插桩”的应用场景
上一节,我们对“字节码插桩”有了一个宏观的认识,知道它发生在编译期的哪个环节。下面我们来了解下它的应用场景。
通过“字节码插桩”,我们可以全局替换目标方法的实现、增加目标方法的逻辑,这种处理方式更加通用彻底且具有兼容性,基于这样的能力,"字节码插桩"具备很大的想象空间:
我们可以为
ImageView
增加大图加载监控,可以为onClick
增加防重复点击逻辑,可以...... 更多案例可以参考:Android 字节码插桩库,也许有你需要的
2. 了解.class文件
前面,我们对“字节码插桩”有了一个大概的印象。而“字节码插桩”操作的对象就是.class
文件,所以有必要先了解下.class
的相关知识。
-
2.1 .class文件的构成
.class
文件是符合JVM
要求的二进制文件,它按顺序的存储着10部分数据,如下图所示:其中的“方法表”存储着类的所有方法,包括每个方法的可见性、方法名、方法签名、方法包含的具体代码等,“字节码插桩”主要对这块进行操作。想更加详细的了解
.class
文件的构成,可以阅读:Java字节码增强探秘 - 字节码的结构 -
2.2 从JVM指令看方法调用
上一节,我们对
.class
的构成有了大概的了解,下面我们看看一个简单的方法与JVM指令之间的关系。下面是一个很普通的
.java
文件,我们先借助javac
命令,把它编译成.class
文件。class A { public void test() { System.out.println("Hello world!"); } }
然后再借助
javap
命令,把上一步产生的.class
文件,反编译成便于阅读的内容。上面两个图分别是反编译后的常量池和方法表。其中图二框框内的代码,就是
test
方法对应的JVM指令:getstatic
、ldc
、invokevirtual
等是代表特定JVM操作的助记符;#7
、#13
、#15
等是.class
文件常量池的索引,指向图一的常量池。通过多个JVM指令的配合,完成
System.out.println("Hello world!");
这个简单的操作。更详细可以阅读:Java字节码增强探秘 - 操作数栈和字节码可以看出,在
.java
中一个简单的方法,实际运行时由多条JVM指令组成,而常规“字节码插桩”正是通过增加或修改JVM指令来改变函数的行为。
3. 利用ASM框架修改方法逻辑
前面,我们知道了“字节码插桩”发生于编译期,主要对.class
文件的“方法表”进行操作。实际上.class
文件是由0
和1
组成的二进制文件,对其进行操作是十分复杂的,所以也就有了一些框架如ASM
、Javassist
等。这些框架将复杂的数据操作,封装成一个个方法,通过这些方法我们就可以完成对.class
文件的修改。
从AGP-8
开始,官方默认使用ASM
,所以下面围绕ASM
进行介绍。
-
3.1 ASM框架
众所周知,类由字段、方法等组成,因此
.class
文件的组成部分也有“字段表”、“方法表”。所以不出意外得ASM
也有类似的对应关系:ClassVisitor
对应类,MethodVisitor
对应方法,FieldVisitor
对应字段...... 通过XXXVisitor
即可对目标部分做操作,比如通过MethodVisitor
就可以对类的方法进行操作。那具体怎么操作呢?前面我们知道了,“字节码插桩”是对JVM指令进行操作,
ASM
以JVM指令为单位,提供了很多visitXXX
的方法,调用这些方法即可插入相应的JVM指令。比如调用MethodVisitor.visitMethodInsn(...)
即可在当前方法中,插入调用其他方法的JVM指令。关于
XXXVisitor
和visitXXX
的更详细讲解,可以阅读:AOP 利器 ASM 基础入门 -
3.2 修改方法逻辑实战
前面,我们了解了
ASM
。下面我们通过一个简单的例子,介绍如何利用ASM
修改JVM指令,从而修改方法逻辑。如上图所示,左边是源代码,右边是一个
AndroidStudio
插件:ASM Bytecode Viewer Support Kotlin。利用这个插件,我们可以查看当前.java
文件对应的ASM
代码,怎么理解这个对应关系呢?以右边框框为例子,通过执行这些ASM
代码,我们就可以往.class
中插入test
这个方法。假设我们的需求是在打印
Hello world!
后打印Hello engineer!
。class A { public void test() { System.out.println("Hello world!"); System.out.println("Hello engineer!"); // 新增一行代码 } }
那么我们可以利用上述插件,将前后
.java
文件处理成ASM
代码,然后通过比较直观的找出我们需要的ASM
代码上面两个图分别是修改前后的
ASM
代码,通过对比不难发现新增的ASM
代码(红色框框部分),执行新增的ASM
代码就可以往test
方法中插入System.out.println("Hello engineer!")
。执行的时机就是:当
MethodVisitor
遍历到上图黄色框框指令时。具体代码如下:以插入一行代码为例,上面介绍了:如何利用插件找到需要新增的
ASM
代码、如何在MethodVisitor
将新增的ASM
代码插入。至于MethodVisitor
如何与AGP
建立起联系,从而对所有目标方法进行处理,可以阅读:Transform 被废弃,ASM 如何适配?
4. 了解Gradle
插件开发
前面,我们知道了如何使用ASM
修改方法逻辑,为了使好不容易写出来的代码更加易于迁移、便于其他项目使用,我们还需要了解下Gradle
插件开发。
Gradle
插件可以理解为“代码搬运工”,类似build.gradle
中的第三方库依赖。只不过Gradle
插件用在编译阶段,整个编译阶段可能会涉及很多插件,其中就有经常提到的AGP
。
关于插件开发,推荐先阅读“关于 Gradle 你应该知道的知识点”了解Gradle
的基础知识,对插件在编译阶段的定位有一定了解,然后再阅读Gradle自定义插件并上传到JitPack了解插件开发。
5. 总结
本文从一个小白的角度,介绍“字节码插桩”需要知道的知识点,但碍于每个知识点都不是三言两语能讲清楚的,因此在每个章节都附带更加详细的文章,感兴趣可以进一步学习。有什么不对的地方欢迎指出~
“字节码插桩”能统一处理所有代码,对“代码不集中,排查难度大”、“第三方库不好修改”、“后续代码兼容”等问题,有比较好的处理效果。再配合Gradle
插件,能实现“一次开发,到处使用”的效果。
“字节码插桩”虽然学习起来比较难,但是鉴于“字节码插桩”的强大能力,能给实际开发带来较大的想象空间,拓宽解决思路,还是比较推荐学习掌握的。
6.摄影环节
- 时间:2023.11.26
- 地点:广州 / 南沙慧谷超级堤
- 自言自语:之前看过别人拍的类似照片,于是看到芦苇丛我就蹲进去了,拍了好久终于拍到了,满足!