Android ASM

news2024/11/25 14:32:24

文章目录

  • 逆波兰表达式与字节码的关系
    • 中缀表达式转换为逆波兰表达式(后缀表达式)的过程
    • 逆波兰表达式求值过程
  • 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_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标识这是一个模块
  • 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 类型类型描述符
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;
  • 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()

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()

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。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/92831.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

原生小程序canvas生成图片、保存到本地

今天在视频中看到一个跳动的小球的效果&#xff0c;感觉挺好玩的。于是自己也实现了一个&#xff0c;感觉还是好玩&#xff0c;就想来分享一番&#xff1b;小伙伴们可以来看一下。这次主要为大家玩一下radial-gradient和动画阴影的调试。 效果呈上 代码来了 大家可以先仔细阅…

Docker安装(图文教程)

一、Docker简介 1、Docker是什么 &#xff08;1&#xff09;Docker是一种虚拟化容器技术。Docker基于镜像&#xff0c;可以秒级启动各种容器。每一种容器都是一个完整的运行环境&#xff0c;容器之间互相隔离。&#xff08;2&#xff09;在Docker的官方&#xff0c;提供了很多容…

Java培训之Nginx启动

1. Nginx启动 启动问题 进入/usr/local/nginx/sbin目录&#xff0c;运行命令./nginx 即可启动nginx nginx无法启动: libpcre.so.1/libpcre.so.0: cannot open shared object file解决办法 Java培训之Nginx启动 解决方法&#xff1a; ln -s /usr/local/lib/libpcre.so.1 /l…

web前端网页设计期末课程大作业:HTML旅游网页主题网站设计——酒店主题网站设计—酒店阳光温馨网站(5页)HTML+CSS+JavaScript

&#x1f468;‍&#x1f393;学生HTML静态网页基础水平制作&#x1f469;‍&#x1f393;&#xff0c;页面排版干净简洁。使用HTMLCSS页面布局设计,web大学生网页设计作业源码&#xff0c;这是一个不错的旅游网页制作&#xff0c;画面精明&#xff0c;排版整洁&#xff0c;内容…

[附源码]Nodejs计算机毕业设计基于的汉服服装租赁系统Express(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分…

32-Vue之ECharts-雷达图

ECharts-雷达图前言雷达图特点雷达图的基本实现雷达图的常见效果显示数值区域面积绘制类型完整代码前言 本篇来学习写雷达图 雷达图特点 可以用来分析多个维度的数据与标准数据的对比情况 雷达图的基本实现 ECharts 最基本的代码结构定义各个维度的最大值准备具体产品的数…

Python编程|手把手教植物大战僵尸,代码开源

前言 如题&#xff0c;手把手教Python实现植物大战僵尸游戏&#xff0c;代码简单易学&#xff0c;无需额外安装Python包&#xff0c;只要有pygame即可&#xff0c;文末获取全部素材及源代码~ 视频演示效果&#xff1a;https://www.bilibili.com/video/BV1cG411u755/?spm_id_…

并发编程之深入理解ReentrantLock和AQS原理

AQS&#xff08;AbstractQueuedSynchronizer&#xff09;在并发编程中占有很重要的地位&#xff0c;可能很多人在平时的开发中并没有看到过它的身影&#xff0c;但是当我们有看过concurrent包一些JDK并发编程的源码的时候&#xff0c;就会发现很多地方都使用了AQS&#xff0c;今…

(文章复现)6计及源荷不确定性的电力系统优化调度(MATLAB程序)

目录 参考文章&#xff1a; 代码主要内容&#xff1a; 主程序&#xff1a; 结果图&#xff1a; 参考文章&#xff1a; 考虑源荷两侧不确定性的含风电电力系统低碳调度——崔杨&#xff08;2020&#xff09; 代码主要内容&#xff1a; 参照考虑源荷两侧不确定性的含风电的…

JAVA基础讲义06-面向对象

面向对象一、编程思想什么是编程思想面向过程和面向对象面向过程编程思想面向过程思想面向过程实现应用场景面向过程特点面向过程代表语言面向对象介绍面向对象编程思想面向对象的三大特征面向对象思想总结什么是编程面向对象分析方法分析问题的思路和步骤二、类和对象类类的概…

它破解了AI作画的中文语料难题,AIGC模型讲解(以世界杯足球为例)

目录1 扩散模型与AI绘画2 中文语料的挑战3 昆仑天工&#xff1a;AIGC新思路3.1 主要特色3.2 模型蒸馏3.3 编解码与GPT3.4 stable-diffusion3.5 性能指标4 体验中文AI绘画模型5 展望1 扩散模型与AI绘画 AI绘画发展历史始于20世纪60年代&#xff0c;当时人工智能研究者们尝试使用…

springboot启动流程源码分析

一、引入思考的问题 1、springboot未出现之前&#xff0c;我们在在spring项目中如果要使用数据源&#xff08;比如我们使用druid&#xff09;&#xff0c;需要做哪些事情呢&#xff1f; &#xff08;1&#xff09;引入druid的jar包 &#xff08;2&#xff09;配置数据源的参…

微服务调用工具

微服务调用工具目录概述需求&#xff1a;设计思路实现思路分析1.A2.B3.C参考资料和推荐阅读Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip hardness,make a better result,wait for change,challenge Survive…

Postman API测试工具 - 初认知 基本使用(一)

Postman - API测试工具 初认知&#xff08;一&#xff09; 文章目录Postman - API测试工具 初认知&#xff08;一&#xff09;一、什么是Postman&#xff1f;二、如何下载Postman&#xff1f;三、Postman的使用四、处理GET请求&#xff1a;五、处理POST请求总结一、什么是Postm…

Python 缩进语法的起源:上世纪 60-70 年代的大胆创意

上个月&#xff0c;Python 之父 Guido van Rossum 在推特上转发了一篇文章《The Origins of Python》&#xff0c;引起了我的强烈兴趣。 众所周知&#xff0c;Guido 在 1989 年圣诞节期间开始创造 Python&#xff0c;当时他就职于荷兰数学和计算机科学研究学会&#xff08;简称…

MySQL之聚合查询和联合查询

一、聚合查询&#xff08;行与行之间的计算&#xff09; 1.常见的聚合函数有&#xff1a; 函数 说明 count 查询到的数据的数量 sum 查询到的数据的总和&#xff08;针对数值&#xff0c;否则无意义&#xff09; avg 查询到的数据的平均值&#xff08;针对数值&#xf…

北京智和信通 | 无人值守的IDC机房动环综合监控运维

随着信息技术的发展和全面应用&#xff0c;数据中心机房已成为各大企事业单位维持业务正常运营的重要组成部分&#xff0c;网络设备、系统、业务应用数量与日俱增&#xff0c;规模逐渐扩大&#xff0c;一旦机房内的设备出现故障&#xff0c;将对数据处理、传输、存储以及整个业…

极光笔记 | 以静制动:行为触发营销助力用户转化

01、营销人&#xff0c;你是否饱受困扰&#xff1f; 作为营销人的你&#xff0c;从996到007&#xff0c;每天从早忙到晚&#xff0c;但还是没办法把访客转化成客户&#xff1f; 作为营销人的你&#xff0c;想通过APP通知、短信、邮件、公众号消息等方式&#xff0c;把所有能想…

牛客题霸sql入门篇之条件查询(二)

牛客题霸sql入门篇之条件查询(二) 2 基础操作符 2.1 查找学生是北大的学生信息 2.1.1 题目内容 2.1.2 示例代码 SELECT device_id,university FROM user_profile WHERE university北京大学2.1.3 运行结果 2.1.4 考察知识点 WHERE子句中可以写查询的条件,用于筛选出符合的…

java SPI机制的使用及原理

本片文章是针对dubbo SPI机制深入分析的平滑过渡的作用。当然咱们主要是学习优秀的思想&#xff0c;SPI就是一种解耦非常优秀的思想&#xff0c;我们可以思考在我们项目开发中是否可以使用、是否可以帮助我们解决某些问题、或者能够更加提升项目的框架等 一、SPI是什么 SPI&a…