本文从一个小白的角度出发,介绍学习"字节码插桩"这个技术,所需要的了解掌握的背景、应用场景、相关知识点等内容。
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文件。javaclass 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即可对目标部分做操作,那具体怎么操作呢?前面我们知道了,"字节码插桩"是对JVM指令进行操作,ASM以JVM指令为单位,提供了很多visitXXX的方法,调用这些方法即可插入相应的JVM指令。关于visitXXX的内容,不展开讲述,更多可以阅读:AOP 利器 ASM 基础入门 -
3.2 修改方法逻辑实战
前面,我们了解了
ASM。下面我们通过一个简单的例子,介绍如何利用ASM修改JVM指令,从而修改方法逻辑。
如上图所示,左边是源代码,右边是一个
AndroidStudio插件:ASM Bytecode Viewer Support Kotlin。利用这个插件,我们可以查看当前.java文件对应的ASM代码,怎么理解这个对应关系呢?以右边框框为例子,通过执行这些ASM代码,我们就可以往.class中插入test这个方法。假设我们的需求是在打印
Hello world!后打印Hello engineer!。csharpclass 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. 总结
本文从一个小白的角度,介绍"字节码插桩"需要知道的知识点,整体算比较全面,但碍于每个知识点都不是三言两语能讲清楚的,因此在每个章节都附带更加详细的文章,感兴趣可以进一步学习。有什么不对的地方欢迎指出~
"字节码插桩"虽然学习起来比较难,但是鉴于"字节码插桩"的强大能力,能给实际开发带来较大的想象空间,拓宽解决思路,还是比较推荐学习掌握的。