字节码插装原理
字节码插装本质上是换了一种编程方式,最初大家是通过Java,kotlin等高级语言编程。运行在Java虚拟机上的语言并不是直接被虚拟机加载,而是要先通过javac,kotlinc编译器编译成后缀为.class的字节码文件后才能被虚拟机加载。
字节码插装是在编译完成后对生成的class文件进行二次操作。
Java虚拟机只与class文件打交道。class文件,也可以看作一种语言,相比于更贴近人类的高级语言,它的可读性更差。但也是具有语法和结构的。在Java中创建对象 或 声明一个变量等操作最终会由多条字节码指令组合完成。
总之呢字节码插装就是程序员面对class文件进行编程
举个小例子感受一下字节码指令
代码非常简单,两个静态方法,add()加法计算,append()字符串拼接。先使用Javac工具把Test.java编译为Test.class,在通过Javap查看Test.class的字节码指令
---------------------java 源文件-----------------------
public class Test {
public static int add(int num1, int num2) {
return num1 + num2;
}
public static String append(String str1, String str2) {
return str1+str2;
}
}
---------------------命令行操作-----------------------
javac Test.java
javap -c Test
---------------------字节码指令-----------------------
public class com.example.architecture.Test {
public com.example.architecture.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static int add(int, int);
Code:
0: iload_0
1: iload_1
2: iadd
3: ireturn
public static java.lang.String append(java.lang.String, java.lang.String);
Code:
0: new #2 // class java/lang/StringBuilder
3: dup
4: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
7: aload_0
8: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
11: aload_1
12: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: invokevirtual #5 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
18: areturn
}
字节码指令分析:
生成了三个方法,除自定义的add(),append()方法外,还有默认的构造方法
add方法比较简单
首先调用 iload_0
这是什么意思呢,从局部变量表第一个位置把数据加载到栈帧,在字节码中i表示int类型,还有d表示double,c表示char。
iload_1
与iload_0
一致
iadd
表示进行 加法运算
ireturn
返回int类型的数据
append方法稍微复杂一点
new
表示创建对象实例,后面跟着类的全限定名 在例子中创建了java/lang/StringBuilder
dup
复制栈顶数据并重新压入栈顶
invokespecial
用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和 父类方法
aload_0
a表示引用类型,把引用类型加入栈帧中,也就是刚刚创建的StringBuilder对象
invokevirtual
用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派), 这也是Java语言中最常见的方法分派方式,
指令相当于一个方法,在调用指令的同时还必须要告诉指令,我要调用的方法签名,结合后面注释的信息才是一次完整的通过指令调用方法,如下是调用StringBuilder.append
的玩则完整指令
invokevirtual
字节码指令 调用对象的实例方法java/lang/StringBuilder.append
表明调用类的那个方法(Ljava/lang/String;)
方法参数 L表示引用类型 后跟类的全限定名Ljava/lang/StringBuilder;
方法返回值 规则与方法参数一致
invokevirtual java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
方法签名的具体规则如下
Android字节码插装准备
上一节体验了一下字节码相关的知识,字节码插装的本质就是换了一门语言写代码,由原来的Java,kotlin等高级语言,换为字节码这种更接近底层的语言。
Android字节码插装有两个前提
- 获取class文件
- 对字节码内容进行操作
在Android上使用字节码插装的第一个问题就是如何获取到class文件,测试情况下可以手动使用命令手动编译文件,但实际开发中这样做手动编译没有意义。
这里涉及到Android构建流程的知识,在打包apk的过程中有一步骤是将Java,kotlin源文件编译为class文件。
Android打包过程基于Gradle,Google官方实现自己的插件AGP,同时开放api允许开发者监听构建流程,干预编译过程,能够获取到编译后的class文件。
如何获取class文件的问题解决了。
对class文件进行操作,可以使用aspectJ,javassit,asm等三方框架,aspectJ 与 javassit对字节码操作封装程度较高使用起来比较容易。 asm的封装程度最低,需要一定的字节码相关知识,不过asm的性能最好。
在AGP7.0之前,使用transform API时,开发者可以任意选择三方框架。AGP7.0之后,transformAPI被废弃,8.0后被移除。
官方提供的新APIAsmClassVisitorFactory
instrumentation
,相比于transform API,新api官方进行了封装,使用更简单,性能更高。但是与asm强关联,以后字节码插装只能使用asm框架,虽然使用麻烦一点,但性能更好,熟悉了字节码之后也就那样,而且有插件可以查看字节码辅助开发。
小总结:
Android字节码插装需要三个前置知识
- 了解Gradle知识,知道如何自定义插件
- 了解AGP监听构建流程,获取字节码文件相关api
- asm框架的使用
学习流程介绍(水一节)
原本想把新API各个类,整个流程讲讲,可惜能力有限,没有自己的思考扒别人的内容也没意思,就改为把学习插装用到资源介绍一下,学完看懂demo,上手肯定是没问题
视频,看完后asm怎么用应该是可以入门了
【Android进阶系统学习】:字节码插桩技术实现自动化方法耗时记录_哔哩哔哩_bilibili
Transform API介绍
其实 Gradle Transform 就是个纸老虎 —— Gradle 系列(4) - 掘金 (juejin.cn)
AGP新api AsmClassVisitorFactory
介绍,文末有大佬文章可看
刚学会Transform,你告诉我就要被移除了 - 掘金 (juejin.cn)
AsmClassVisitorFactory
实战,有demo
Transform 被废弃,ASM 如何适配? - 掘金 (juejin.cn)
Gradle相关知识,文末有大佬的系列文章都不错的 可看,Gradle其实挺复杂的弄懂概念需要一段时间
写给小白的Gradle指南 - 掘金 (juejin.cn)
Gradle插件
Java基础