当然上面的十六进制文件显然不具备可阅读性,所以我们可以通过 javap -verbose Test
来反编译,有兴趣的可以自己试一试,就可以看到上面说的十个部分,由于我们做字节码插桩一般和方法表关联比较大,所以我们下面着重看一下方法表
,下面是反编译后的add()方法:
可以看到包括三部分:
Code
: 这里部分就是方法里的JVM指令操作码,也是最重要的一部分,因为我们方法里的逻辑实际上就是一条一条的指令操作码来完成的。这里可以看到我们的add方法是通过9条指令操作码完成的。当然插桩重点操作的也是这一块,只要能修改指令,也就能操控任何代码了。LineNumberTable
: 这个是表示行号表。是我们的java源码与指令行的行号对应。比如我们上面的add方法java源码里总共有三行,也就是上图中的line10、line11、line12,这三行对应的JVM指令行数。有了这样的对应关系后,就可以实现比如Debug调试的功能,指令执行的时候,我们就可以定位到该指令对应的源码所在的位置。LocalVariableTable
:本地变量表,主要包括This和方法里的局部变量。从上图可以看到add方法里有this、j、k三个局部变量。
由于JVM指令集是基于栈的,上面我们已经了解到了add方法的逻辑编译为class文件后变成了9个指令操作码,下面我们简单看看这些指令操作码是如何配合操作数栈+本地变量表+常量池
来执行add方法的逻辑的:
按顺序执行9条指令操作码:
- 0:把数字2入栈
- 1:将2赋值给本地变量表中的j
- 2、3:获取常量池中的m入栈
- 6:将本地变量表中的j入栈
- 7、8:将m和j相加,然后赋值给本地变量表中的k
- 9、10:将本地变量表中的k入栈,并return
好的,关于java字节码的暂时就简单介绍这些,主要是让我们基本了解字节码文件的结构,以及编译后代码时如何运行的。而ASM可以通过操作指令码来生成字节码或者插桩,当你可以利用ASM来接触到字节码,并且可以利用ASM的api来操控字节码时,就有很大的自由度来进行各种字节码的生成、修改、操作等等,也就能产生很强大的功能。
三、Gradle plugin + Transform
上面对于插桩框架的选择,我们通过对比最终选择了ASM,但是ASM只负责操作字节码,我们还需要通过自定义gradle plugin的形式来干预编译过程,在编译过程中获取到所有的class文件和jar包,然后遍历他们,利用ASM来修改字节码,达到插桩的目的。
那么干预编译的过程,我们的第一个念头可能就是,对class转为dex的任务进行hook
,在class转为dex之前拿到所有的class文件,然后利用ASM对这些字节码文件进行插桩,然后再把处理过的字节码文件作为transformClassesWithDex
任务的输入即可。这种方案的好处是易于控制,我们明确的知道操作的字节码文件是最终的字节码,因为我们是在transformClassesWithDex
任务的前一刻拿到字节码文件的。缺点就是,如果项目开启了混淆,那么在transformClassesWithDex
任务的前一刻拿到的字节码文件显然是经过了混淆了的,所以利用ASM操作字节码的时候还需要mapping文件进行配合才能找到正确的插桩点,这一点比较麻烦。
幸亏gradle还为我们提供了另一种干预编译转换过程的方法:Transform
.其实我们稍微翻一下gradle编译过程的源码,就会发现一些我们熟知的功能都是通过Transform来实现的。还有一点,就是关于混淆的问题,上面我们说了如果通过hook transformClassesWithDex
任务的方式来实现插桩,开启混淆的情况下会出