文章目录
- 逆波兰表达式与字节码的关系
- 中缀表达式转换为逆波兰表达式(后缀表达式)的过程
- 逆波兰表达式求值过程
- ASM 的使用
- ASM 常用 api 说明
- ClassWriter
- 构造函数传参 flags 的作用
- 定义类的属性:visit()
- 定义类的方法:visitMethod()
- 定义变量:visitField()
- class 已经使用结束:visitEnd()
- 生成 class 字节码:toByteArray()
- MethodVisitor
- 开始生成字节码:visitCode()
- 设置 Label:visitLabel()
- 定义源代码中的行号与对应的指令:visitLineNumber()
- 对变量进行加载和存储的指令操作:visitVarInsn()
- 对一个方法执行指令操作:visitMethodInsn()
- 执行对操作数栈的指令:visitInsn()
- 对局部变量设置:visitLocalVariable()
- 设置本地方法最大操作数栈和最大本地变量表:visitMaxs()
- 生成方法结束:visitEnd()
- 使用 ASM 插入打印方法执行时间
- 使用 ASM Bytecode Viewer 生成代码
- 总结
逆波兰表达式与字节码的关系
表达式一般由操作数(Operand)、运算符(Operator)组成,例如算术表达式中,通常把运算符放在两个操作数的中间,这称为中缀表达式(Infix Expression),如 A + B。
波兰数学家 Jan Lukasiewicz 提出了另一种数学表示法,它有两种表示形式:
-
把运算符写在操作数之前,称为波兰表达式(Polish Expression)或前缀表达式(Prefix Expression),如 +AB
-
把运算符写在操作数之后,称为逆波兰表达式(Reverse Polish Expression)或后缀表达式(Suffix Expression),如 AB+
其中,逆波兰表达式在编译技术中有着普遍的应用。
在编译中比如将 java 转换成 class 文件的过程,从算法中可以理解为就是将中缀表达式转换为逆波兰表达式的过程。
中缀表达式转换为逆波兰表达式(后缀表达式)的过程
中缀表达式转换为逆波兰表达式的算法如下:
1、从左至右扫描中缀表达式
2、若读取的是操作数,则判断该操作数的类型,并将该操作数存储操作数栈
3、若读取的是运算符
(1)该运算符为左括号 “(”,则直接存入运算符栈
(2)该运算符为右括号 “)”,则输出运算符栈中的运算符到操作数栈,直到遇到左括号为止
(3)该运算符为非括号运算符
-
若运算符栈栈顶的运算符为括号,则直接存入运算符栈
-
若比运算符栈栈顶的运算符优先级高或相等,则直接存入运算符栈
-
若比运算符栈栈顶的运算符优先级低,则输出栈顶运算符到操作数栈,并将当前运算符压入运算符栈
4、当表达式读取完成后运算符栈中尚有运算符时,则依序取出运算符到操作数栈,直到运算符栈为空
文字描述转换算法你可能不好理解,我们用一个案例来分析下上面的算法。
假设我们的代码要计算一个中缀表达式 a + b * (c - d) - e / f
,现在要将它转换为逆波兰表达式。首先会准备两个栈,一个是存放扫描时扫到的运算符,称为运算符栈,一个是用于存放数据的操作数栈。
首先是从左往右扫描中缀表达式,因为运算符栈中栈顶的运算符优先级比下面的运算符高,所以此时还不会弹栈开始运算;当遇到右括号时就会开始弹栈计算,此时运算 c - d
,直到遇到左括号时结束,运算结果 c - d
压回操作数栈。如下图所示:
计算完 c - d
后继续找到 *
和 +
运算符,继续计算 b * (c - d)
后,将结果压入操作数栈;然后再计算 a + b * (c - d)
,将结果压入操作数栈。如下图所示:
剩下的也同理,运算 e / f
和最终结果 a + b * (c-d) - e / f
,所以最终就能得到一个逆波兰表达式 a b c d - * + e f / -
。如下图所示:
逆波兰表达式求值过程
逆波兰表达式求值算法如下:
1、循环扫描语法单元的项目
2、如果扫描的项目是操作数,则将其压入操作数栈,并扫描下一个项目
3、如果扫描的项目是一个二元运算符,则对栈的顶上两个操作数执行该运算
4、如果扫描的项目是一个一元运算符,则对栈的最顶上操作数执行该运算
5、将运算符结果重新压入堆栈
6、重新步骤 2-5,堆栈中即为结果值
还是用上面得到的逆波兰表达式案例来分析计算步骤。
有一个逆波兰表达式 a b c d - * + e f / -
,然后就可以在操作数栈从左到右扫描,遇到运算符就做计算后重新压入栈顶,直到计算结束。如下图所示:
如果我们有将 java 转换成 class 查看字节码,就会发现最终 字节码的展示顺序就是逆波兰表达式:
因为字节码执行顺序是逆波兰表达式的扫描顺序,所以如果我们要了解 ASM 如何修改字节码,就需要先了解什么是逆波兰表达式,ASM 修改字节码也要遵循逆波兰表达式的顺序修改。
ASM 的使用
所谓的 ASM ,其实就是如何生成或修改一个 class 文件的工具,包括对 class 里的成员变量或者方法进行增加或修改,相比于 javassist,ASM 最大的好处就是性能方面优于 javassist,但随之带来的就是需要精通 class 文件格式和 JVM 指令集。
在使用 ASM 前需要引入库:
implementation 'org.ow2.asm:asm:9.3'
implementation 'org.ow2.asm:asm-commons:9.3'
ASM 常用 api 说明
在 ASM 中最主要的就是 ClassReader、ClassWriter 两个类,ClassReader 负责读取分析字节码,ClassWriter 负责修改生成字节码。更具体的的 ASM api 使用可以查看官方文档:ASM 开发文档。
我们用一个案例在简单讲解 ASM 常用的 api。
现在我们需要生成 User 对象的 class 文件,用 ASM 编写生成文件的字节码如下:
// ASM Bytecode Viewr 生成的代码,不需要我们自己写,后面节点会讲到
ClassWriter classWriter = new ClassWriter(0);
classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER, "com/example/lib/User", null, "java/lang/Object", null);
classWriter.visitSource("User.java", null);
MethodVisitor methodVisitor = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(3, label0);
methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
methodVisitor.visitInsn(Opcodes.RETURN);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLocalVariable("this", "Lcom/example/lib/User;", null, label0, label1, 0);
methodVisitor.visitMaxs(1, 1);
methodVisitor.visitEnd();
classWriter.visitEnd();
byte[] bytes = classWriter.toByteArray();
// 将生成的字节码输出到指定的本机目录下的文件
try {
FileOutputStream fos = new FileOutputStream("/Volumes/MacintoshData/develop/android/project/demo/lib/src/main/java/com/example/lib/User.class");
fos.write(bytes);
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
最终生成的代码如下:
User.class
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.example.lib;
public class User {
public User() {
}
}
上面的字节码生成代码使用到比较多的 api,有 ClassWriter 和 MethodVisitor,我们一个个了解下它们具体的用法和方法的作用。
因为分析中还涉及到 JVM 相关的知识,建议可以简单先了解下 JVM 相关的知识:JVM 运行时数据区(栈和堆)
ClassWriter
构造函数传参 flags 的作用
public ClassWriter(final int flags)
flags 参数的作用:
数值 | 作用 |
---|---|
flags == 0 | 你必须自己计算栈帧和局部变量表以及操作数堆栈的大小,也就是你要自己调用 visitMax() 和 visitFrame() 方法 |
flags == ClassWriter.COMPUTE_MAXS | 局部变量表和操作数堆栈部分的大小会为你计算,还需要调用 visitFrames() 设置栈帧 |
flags == ClassWriter.COMPUTE_FRAMES | 所有的内容都是自动计算的,你不必调用 visitFrame() 和 visitMax(),即 COMPUTE_FRAMES 包含 COMPUTE_MAXS |
需要注意的是,使用 ClassWriter.COMPUTE_MAXS 会使 ClassWriter 的速度慢 10%,ClassWriter.COMPUTE_FRAMES 会慢 20%。
定义类的属性:visit()
classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER, "com/example/lib/User", null, "java/lang/Object", null);
@Override
public final void visit(
final int version,
final int access,
final String name,
final String signature,
final String superName,
final String[] interfaces)
-
version:Java 版本号。例如 V1_8 代表 Java 8
-
access:class 访问权限,一般默认都是 ACC_PUBLIC | ACC_SUPER。具体说明如下
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为 public 类型 |
ACC_FINAL | 0x0010 | 是否被声明为 final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用 invokespecial 字节码指令的新语义,invokespecial 指令的语义在 jdk 1.0.2 发生过改变,为了区别这条指令使用哪种语义,jdk 1.0.2 之后编译出来的类的这个标志都必须为 true |
ACC_INTERFACE | 0x0200 | 是否为接口 |
ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型,对于接口或抽象类来说,此标志值为 true,其他类型值为 false |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生的 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
ACC_MODULE | 0x8000 | 标识这是一个模块 |
-
name:class 文件名。例如
com/example/lib/User
,包名加类名 -
signature:类的签名,除非是泛型类或实现泛型接口,一般默认 null
-
superName:继承的类,所有类默认继承 Object。例如
java/lang/Object
,如果是继承自己写的类 Animal,那就是com/example/lib/Animal
-
interfaces:实现的接口。例如实现自己写的接口 IPrint,那就是
new String[] {"com/example/lib/IPrint"}
定义类的方法:visitMethod()
classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
@Override
public final MethodVisitor visitMethod(
final int access,
final String name,
final String descriptor,
final String signature,
final String[] exceptions)
-
access:方法的访问权限。例如 public、private 等
-
name:方法名。在 class 中有两个特殊的方法名:
<init>
代表的是类的实例构造方法初始化,也就是 new 一个类时肯定会调用的方法。
<clinit>
代表的类的初始化方法,不同与<init>
,它不是显示调用的。因为 Java 虚拟机会自动调用<clinit>
,并且保证在子类的<clinit>
前调用父类的<clinit>
。也就是说,Java 虚拟机中,第一个被执行<clinit>
方法的肯定是 java.lang.Object
注意:
<init>
和<clinit>
执行的时机不一样,<clinit>
的时机早于<init>
,<clinit>
是在类被加载阶段的初始化过程中调用<clinit>
方法,而<init>
方法的调用时在 new 一个类的实例的时候。
- descriptor:方法的描述符。所谓方法的描述符,就是字节码对代码中方法的形参和返回值的一个描述。其实就是一个一一对应的模板:
例如描述符为 (IF)V,表示的是 (表示方法的形参类型描述符)方法的返回值
关于形参的类型描述符如下:
Java 类型 | 类型描述符 |
---|---|
boolean | Z |
char | C |
byte | B |
short | S |
int | I |
float | F |
long | J |
double | D |
Object | Ljava/lang/Object; (L + 包名 + 类名 + ;) |
int[] | [I ([ + I),其他类型同理 |
Object[][] | [[Ljava/lang/Object; ([ + [ + 包名 + 类名 + ;) |
User(自定义类) | Lcom/example/lib/User; (L + 包名 + 类名 + ;) |
方法描述符如下:
方法的声明 | 方法描述符 |
---|---|
void m(int i, float f) | (IF)V |
int m(Object o) | (Ljava/lang/Object;) I |
int[] m(int i, String s) | (ILjava/lang/String;)[I |
Object m(int[] i) | ([I)Ljava/lang/Object; |
-
signature:方法签名。除非方法的参数、返回类型和异常使用了泛型,否则一般为 null
-
exceptions:方法上的异常。如果方法没有声明抛出异常为 null;否则声明了 throw Exception 则为
new String[] {"java/lang/Exception"}
定义变量:visitField()
// 生成一个 private int a = 10;
classWriter.visitField(ACC_PRIVATE, "a", "I", null, null);
@Override
public final FieldVisitor visitField(
final int access,
final String name,
final String descriptor,
final String signature,
final Object value)
-
access:变量的访问权限。例如 public 、private 等
-
name:变量名
-
descriptor:变量的描述符。参考上面 visitMethod() 的说明
-
signature:变量的签名。如果没有使用泛型则为 null
-
value:变量的初始值。该字段仅作用于被 final 修饰的字段,或者接口中声明的变量。其他默认为 null,变量的赋值是通过 MethodVisitor 的 visitFieldInsn()
class 已经使用结束:visitEnd()
classWriter.visitEnd();
生成 class 字节码:toByteArray()
byte[] bytes = classWriter.toByteArray();
MethodVisitor
开始生成字节码:visitCode()
methodVisitor.visitCode();
通常第一个调用,固定格式。
设置 Label:visitLabel()
Label label0 = new Label();
methodVisitor.visitLabel(label0);
public void visitLabel(final Label label)
Label 的作用相当于表示方法在字节码中的位置。
每一个方法都需要一个 Label,用来保证方法调用顺序。
定义源代码中的行号与对应的指令:visitLineNumber()
methodVisitor.visitLineNumber(3, label0);
public void visitLineNumber(final int line, final Label start)
-
line:源代码中对应的行号
-
start:行号对应的字节码指令
对变量进行加载和存储的指令操作:visitVarInsn()
methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
public void visitVarInsn(final int opcode, final int varIndex)
- opcode:对应的变量字节码指令。例如获取一个 int 数值类型的指令对应 iload 0
字节码 | 助记符 | 指令含义 |
---|---|---|
0x15 | iload | 将指定的 int 类型本地变量推送至栈顶 |
0x16 | lload | 将指定的 long 类型本地变量推送至栈顶 |
0x17 | fload | 将指定的 float 类型本地变量推送至栈顶 |
0x18 | dload | 将指定的 double 类型本地变量推送至栈顶 |
0x19 | aload | 将指定的引用类型本地变量推送至栈顶 |
0x1a | iload_0 | 将第一个 int 类型本地变量推送至栈顶 |
0x1b | iload_1 | 将第二个 int 类型本地变量推送至栈顶 |
有获取就会有存储:
字节码 | 助记符 | 指令含义 |
---|---|---|
0x38 | fstore | 将栈顶 float 类型数值存入指定本地变量 |
0x39 | dstore | 将栈顶 double 类型数值存入指定本地变量 |
0x3a | astore | 将栈顶引用类型数值存入指定本地变量 |
0x3b | istore_0 | 将栈顶 int 类型数值存入第一个本地变量 |
0x3c | istore_1 | 将栈顶 int 类型数值存入第二个本地变量 |
- varIndex:变量对应在局部变量表的下标。例如下面的代码:
int a = 1;
int b = 2;
int d = a + b;
字节码指令:
L5
LINENUMBER 13 L5
ICONST_1 // 将 1 变量加载到操作数栈,对应的指令就是 ICONST_1
ISTORE 1 // 将栈顶的值保存到局部变量表第一个位置,对应的指令就是 ISTORE_1
L6
LINENUMBER 14 L6
ICONST_2 // 将 2 变量加载到操作数栈,对应的指令就是 ICONST_2
ISTORE 2 // 将栈顶的值保存到局部变量表第二个位置,对应的指令就是 ISTORE_2
L7
LINENUMBER 15 L7
ILOAD 1 // 取出局部变量表第一个元素到操作数栈(也就是变量 a),对应的指令就是 ILOAD_1
ILOAD 2 // 取出局部变量表第二个元素到操作数栈(也就是变量 b),对应的指令就是 ILOAD_2
IADD // 此时操作数栈的栈顶就有 a 和 b 两个元素,执行指令 IADD,就会把栈顶的两个元素相加并将结果压入栈顶
ISTORE 3 // 将栈顶的值保存到局部变量表第三个位置
对一个方法执行指令操作:visitMethodInsn()
methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
public void visitMethodInsn(
final int opcode,
final String owner,
final String name,
final String descriptor,
final boolean isInterface)
可以执行的指令如下:
字节码 | 助记符 | 指令含义 |
---|---|---|
0xb6 | invokevirtual | 调用实例方法 |
0xb7 | invokespecial | 调用超类构造方法,实例初始化方法、私有方法 |
0xb8 | invokestatic | 调用静态方法 |
0xb9 | invokeinterface | 调用接口方法 |
执行对操作数栈的指令:visitInsn()
methodVisitor.visitInsn(Opcodes.RETURN);
public void visitInsn(final int opcode)
opcode 可以执行的指令:
部分指令说明如下:
字节码 | 助记符 | 指令含义 |
---|---|---|
0x5f | swap | 将栈最顶端的两个数值互换(数值不能是 long 或 double 类型) |
0x60 | iadd | 将栈顶两 int 类型数值相加并将结果压入栈顶 |
0x61 | ladd | 将栈顶两 long 类型数值相加并将结果压入栈顶 |
0x62 | fadd | 将栈顶两 float 类型数值相加并将结果压入栈顶 |
0x63 | dadd | 将栈顶两 double 类型数值相加并将结果压入栈顶 |
0x64 | isub | 将栈顶两 int 类型数值相减并将结果压入栈顶 |
0x65 | lsub | 将栈顶两 long 类型数值相减并将结果压入栈顶 |
0x66 | fsub | 将栈顶两 float 类型数值相减并将结果压入栈顶 |
0x67 | dsub | 将栈顶两 double 类型数值相减并将结果压入栈顶 |
0x68 | imul | 将栈顶两 int 类型数值相乘并将结果压入栈顶 |
0x69 | lmul | 将栈顶两 long 类型数值相乘并将结果压入栈顶 |
0x6a | fmul | 将栈顶两 float 类型数值相乘并将结果压入栈顶 |
0x6b | dmul | 将栈顶两 double 类型数值相乘并将结果压入栈顶 |
0x6c | idiv | 将栈顶两 int 类型数值相除并将结果压入栈顶 |
0x6d | ldiv | 将栈顶两 long 类型数值相除并将结果压入栈顶 |
0x6e | fdiv | 将栈顶两 float 类型数值相除并将结果压入栈顶 |
0x6f | ddiv | 将栈顶两 double 类型数值相除并将结果压入栈顶 |
对局部变量设置:visitLocalVariable()
methodVisitor.visitLocalVariable("this", "Lcom/example/lib/User;", null, label0, label1, 0);
public void visitLocalVariable(
final String name,
final String descriptor,
final String signature,
final Label start,
final Label end,
final int index)
-
name:局部变量名
-
descriptor:局部变量名的类型描述符
-
signature:局部变量名的签名。如果没有使用到泛型则为 null
-
start:第一条指令对应于这个局部变量的作用域(包括)
-
end:最后一条指令对应于这个局部变量的作用域(排他)
-
index:局部变量名的下标,也就是局部变量名的行号顺序(从 1 开始)
例如代码:
public void test() {
int a = 1;
int b = 2;
int d = a + b;
}
对应的使用方法如下:
// 每个方法都会默认有一个 this 引用
methodVisitor.visitLocalVariable("this", "Lcom/example/lib/User;", "Lcom/example/lib/User<TT;>;", label0, label4, 0);
methodVisitor.visitLocalVariable("a", "I", null, label1, label4, 1); methodVisitor.visitLocalVariable("b", "I", null, label2, label4, 2); methodVisitor.visitLocalVariable("d", "I", null, label3, label4, 3);
设置本地方法最大操作数栈和最大本地变量表:visitMaxs()
public void test() {
int a = 1;
int b = 2;
int d = a + b;
}
// maxStack == 2 分别是 ICONST_1、IADD 操作
methodVisitor.visitMaxs(2, 4);
public void visitMaxs(final int maxStack, final int maxLocals)
-
maxStack:操作数栈容量大小。例如上面的代码表示操作数栈容量大小为 2 就可以满足上面代码
-
maxLocals:局部变量数量。例如上面的代码局部变量为 this、a、b、d
需要注意的是,visitMaxs() 的调用必须在所有的 MethodVisitor 指令结束后调用。
生成方法结束:visitEnd()
methodVisitor.visitEnd();
通常是作为 MethodVisitor 最后一个调用,固定格式,与 visitCode() 一个最前一个最后。
使用 ASM 插入打印方法执行时间
为了能更好理解 ASM 的处理流程,我们编写一个 demo 使用 ASM 对添加了注解的方法插入方法调用的时间并打印出来。
ASM 插桩主要有几个步骤:
-
先准备编译好的 class 文件
-
使用 ClassReader 读取 class 文件,还需要提供 ClassWriter 修改字节码。调用 classReader.accept(ClassVisitor) 开始插桩
-
ClassWriter 获取修改后的字节码,将字节码重新写回文件
操作步骤如下:
为了方便测试,我们在 AS 新建一个 java library,引入 ASM 的依赖,先提供测试类 Test,然后 build 生成 Test.class 文件:
build.gradle
plugins {
id 'java-library'
}
dependencies {
implementation 'org.ow2.asm:asm:9.3'
implementation 'org.ow2.asm:asm-commons:9.3'
}
java {
sourceCompatibility = JavaVersion.VERSION_1_7
targetCompatibility = JavaVersion.VERSION_1_7
}
// 提供注解设置只在哪个方法使用 ASM 插入代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ASMTest {
}
public class Test {
@ASMTest
public void test() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
具体插桩代码如下:
package com.example.lib;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.Method;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class MainTest {
public static void main(String[] args) {
try {
// 提前准备好要修改的字节码文件
String classFilePath = "/Volumes/MacintoshData/develop/android/project/demo/lib/build/classes/java/main/com/example/lib/Test.class";
FileInputStream fis = new FileInputStream(classFilePath);
// 获取分析器
ClassReader cr = new ClassReader(fis);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// 开始插桩
cr.accept(new MyClassVisitor(Opcodes.ASM7, cw), ClassReader.EXPAND_FRAMES);
// 拿到插桩修改后的字节码
byte[] bytes = cw.toByteArray();
// 字节码写回文件
FileOutputStream fos = new FileOutputStream(classFilePath);
fos.write(bytes);
fos.close();
fis.close();
} catch (Exception e) {
e.printStackTrace();
}
}
// 用来分析类信息
private static class MyClassVisitor extends ClassVisitor {
public MyClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
// 方法执行会回调该方法
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
return new MyMethodVisitor(api, mv, access, name, descriptor);
}
}
// 用来分析方法
// AdviceAdapter 也是 MethodVisitor,只是功能更多而已
// 这些字节码插桩处理用 ASM Bytecode Viewer 就能转换出来,不用自己写
private static class MyMethodVisitor extends AdviceAdapter {
int s;
int e;
boolean inject;
protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
}
@Override
protected void onMethodEnter() {
super.onMethodEnter();
if (!inject) return;
// long s = System.currentTimeMillis();
// INVOKESTATIC java/lang/System.currentTimeMillis ()J
// LSTORE 1
invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
s = newLocal(Type.LONG_TYPE); // 局部变量表申请空间存放变量
storeLocal(s);
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
if (!inject) return;
// long e = System.currentTimeMillis();
// INVOKESTATIC java/lang/System.currentTimeMillis ()J
// LSTORE 3
invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
e = newLocal(Type.LONG_TYPE);
storeLocal(e);
// System.out.println("execute time = " + (e-s) +"ms");
// GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
getStatic(Type.getType("Ljava/lang/System;"), "out", Type.getType("Ljava/io/PrintStream;"));
// NEW java/lang/StringBuilder
newInstance(Type.getType("Ljava/lang/StringBuilder;"));
// DUP
dup();
// INVOKESPECIAL java/lang/StringBuilder.<init> ()V
invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"), new Method("<init>", "()V"));
// LDC "execute time = "
visitLdcInsn("execute time = ");
// INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), new Method("append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;"));
// LLOAD 3
loadLocal(e);
// LLOAD 1
loadLocal(s);
// LSUB
math(GeneratorAdapter.SUB, Type.LONG_TYPE);
// INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), new Method("append", "(J)Ljava/lang/StringBuilder;"));
// LDC "ms"
visitLdcInsn("ms");
// INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), new Method("append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;"));
// INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), new Method("toString", "()Ljava/lang/String;"));
// INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
invokeVirtual(Type.getType("Ljava/io/PrintStream;"), new Method("println", "(Ljava/lang/String;)V;"));
}
// 每读到一个注解就执行一次
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
// 如果是我们指定的注解 @ASMTest 就进行插桩
if ("Lcom/example/lib/ASMTest;".equals(descriptor)) {
inject = true;
}
return super.visitAnnotation(descriptor, visible);
}
}
}
运行代码就能得到一个修改后的 Test.class 文件:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.example.lib;
public class Test {
public Test() {
}
@ASMTest
public void test() {
long var1 = System.currentTimeMillis();
try {
Thread.sleep(1000L);
} catch (InterruptedException var6) {
var6.printStackTrace();
}
long var4 = System.currentTimeMillis();
System.out.println("execute time = " + (var4 - var1) + "ms");
}
}
使用 ASM Bytecode Viewer 生成代码
可以发现使用 ASM 编写字节码代码挺复杂的,又需要很了解字节码和 ASM 的 api,有没有能偷懒的方式呢?
这种有规则规律性的问题可以使用 ASM Bytecode Viewer 生成相关的代码。
在 Android Studio 的 Plugin 插件下载页面,可以看到有三个 ASM 的工具:
-
ASM Bytecode Viewer
-
ASM Bytecode Outline
-
ASM Bytecode Viewer Support Kotlin
但在高版本的 Android Studio 用前两种会存在无法生成和报错的情况,可能是新版本 Android Studio 没有做兼容,所以我们 只需要下载使用 ASM Bytecode Viewer Support Kotlin 就行,如果都有下载,也选择只使用这个插件:
使用也非常简单,先在要修改的代码写好需要生成的代码,右键生成 ASM 的代码:
将 ASM 生成的代码 copy 到自己的项目,剩下的就只需要将生成的字节码写回文件就行了,直接运行拿到修改后的字节码 class:
public class MainTest {
public static void main(String[] args) {
insertMethodExecuteTimePrint();
}
private static void insertMethodExecuteTimePrint() {
try {
String classFilePath = "/Volumes/MacintoshData/develop/android/project/demo/lib/build/classes/java/main/com/example/lib/Test.class";
FileInputStream fis = new FileInputStream(classFilePath);
// dump() 是 ASM 生成修改后的字节码代码
byte[] bytes = dump();
// 字节码写回文件
FileOutputStream fos = new FileOutputStream(classFilePath);
fos.write(bytes);
fos.close();
fis.close();
} catch (Exception e) {
e.printStackTrace();
}
}
生成的字节码:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.example.lib;
public class Test {
public Test() {
}
@ASMTest
public void test() {
long s = System.currentTimeMillis();
try {
Thread.sleep(1000L);
} catch (InterruptedException var5) {
var5.printStackTrace();
}
long e = System.currentTimeMillis();
System.out.println("execute time = " + (e - s) + "ms");
}
}
总结
该篇文章从代码的表达式扩展说明 class 字节码指令的顺序就是逆波兰表达式,这对于了解 ASM 执行顺序是比较有帮助的。
为了能更好的理解 ASM 和使用方式,我们简单的罗列了 ASM 工具常用的 api,比如 ClassWriter 和 MethodVisitor,并且使用 ASM 给指定注解的方法插入代码打印出方法的执行时间。
但是编写 ASM 代码是复杂且繁琐的,针对字节码这类有规律规则的数据,一般都会提供相关的自动化生成工具,使用 ASM Bytecode Viewer 可以很方便的生成我们想要的 ASM 代码,加快开发效率。
总体而言使用 ASM 的难度是不大的,操作流程也比较固定,最主要的是需要我们理解 JVM 字节码相关的知识,才能更好的使用好 ASM。