Android ASM

逆波兰表达式与字节码的关系

表达式一般由操作数(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 运行时数据区(栈和堆)

access 访问权限

在下面的节点会有访问权限的方法传参,具体数值如下:

标志名称标志值含义
ACC_PUBLIC0x0001是否为 public 类型
ACC_FINAL0x0010是否被声明为 final,只有类可设置
ACC_SUPER0x0020是否允许使用 invokespecial 字节码指令的新语义,invokespecial 指令的语义在 jdk 1.0.2 发生过改变,为了区别这条指令使用哪种语义,jdk 1.0.2 之后编译出来的类的这个标志都必须为 true
ACC_INTERFACE0x0200是否为接口
ACC_ABSTRACT0x0400是否为 abstract 类型,对于接口或抽象类来说,此标志值为 true,其他类型值为 false
ACC_SYNTHETIC0x1000标识这个类并非由用户代码产生的
ACC_ANNOTATION0x2000标识这是一个注解
ACC_ENUM0x4000标识这是一个枚举
ACC_MODULE0x8000标识这是一个模块

descriptor 描述符

在下面的节点会有描述符的方法传参,描述符其实就是一个一一对应的模板:

例如描述符为 (IF)V,表示的是 (表示方法的形参类型描述符)方法的返回值

关于形参的类型描述符如下:

Java 类型类型描述符
booleanZ
charC
byteB
shortS
intI
floatF
longJ
doubleD
ObjectLjava/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;

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()
// 访问 com/example/lib/User 类
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

  • 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()
User.class

package com.example.lib;

public class User {
					 // 访问构造函数
    public User() {  <- 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:方法的描述符。所谓方法的描述符,就是字节码对代码中方法的形参和返回值的一个描述

  • 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
字节码助记符指令含义
0x15iload将指定的 int 类型本地变量推送至栈顶
0x16lload将指定的 long 类型本地变量推送至栈顶
0x17fload将指定的 float 类型本地变量推送至栈顶
0x18dload将指定的 double 类型本地变量推送至栈顶
0x19aload将指定的引用类型本地变量推送至栈顶
0x1aiload_0将第一个 int 类型本地变量推送至栈顶
0x1biload_1将第二个 int 类型本地变量推送至栈顶

有获取就会有存储:

字节码助记符指令含义
0x38fstore将栈顶 float 类型数值存入指定本地变量
0x39dstore将栈顶 double 类型数值存入指定本地变量
0x3aastore将栈顶引用类型数值存入指定本地变量
0x3bistore_0将栈顶 int 类型数值存入第一个本地变量
0x3cistore_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()
User.class

package com.example.lib;

public class User {
					 // 调用构造方法
    public User() {  <- 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)

可以执行的指令如下:

字节码助记符指令含义
0xb6invokevirtual调用实例方法
0xb7invokespecial调用超类构造方法,实例初始化方法、私有方法
0xb8invokestatic调用静态方法
0xb9invokeinterface调用接口方法
执行对操作数栈的指令:visitInsn()
methodVisitor.visitInsn(Opcodes.RETURN);

public void visitInsn(final int opcode)

opcode 可以执行的指令:

在这里插入图片描述

部分指令说明如下:

字节码助记符指令含义
0x5fswap将栈最顶端的两个数值互换(数值不能是 long 或 double 类型)
0x60iadd将栈顶两 int 类型数值相加并将结果压入栈顶
0x61ladd将栈顶两 long 类型数值相加并将结果压入栈顶
0x62fadd将栈顶两 float 类型数值相加并将结果压入栈顶
0x63dadd将栈顶两 double 类型数值相加并将结果压入栈顶
0x64isub将栈顶两 int 类型数值相减并将结果压入栈顶
0x65lsub将栈顶两 long 类型数值相减并将结果压入栈顶
0x66fsub将栈顶两 float 类型数值相减并将结果压入栈顶
0x67dsub将栈顶两 double 类型数值相减并将结果压入栈顶
0x68imul将栈顶两 int 类型数值相乘并将结果压入栈顶
0x69lmul将栈顶两 long 类型数值相乘并将结果压入栈顶
0x6afmul将栈顶两 float 类型数值相乘并将结果压入栈顶
0x6bdmul将栈顶两 double 类型数值相乘并将结果压入栈顶
0x6cidiv将栈顶两 int 类型数值相除并将结果压入栈顶
0x6dldiv将栈顶两 long 类型数值相除并将结果压入栈顶
0x6efdiv将栈顶两 float 类型数值相除并将结果压入栈顶
0x6fddiv将栈顶两 double 类型数值相除并将结果压入栈顶
对局部变量设置:visitLocalVariable()
User.class

package com.example.lib;

public class User {

    public User() {  
    	// 访问变量 this	<- 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。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值