springASMAndroid

ASM Core Api 详解

2018-05-16  本文已影响504人  Whyn

前言

前面一篇文章 ASM 简介ASM 框架做了简单的介绍。

本篇文章主要对该框架的 Core Api 其中重要的一些类进行详细的介绍,让大家可以更得心应手的使用 ASM

在开始之前,让我们先回顾一下 ASM Core Api 调用流程:

  1. ASM 提供了一个类ClassReader可以方便地让我们对class文件进行读取与解析;

  2. ASMClassReader解析class文件过程中,解析到某一个结构就会通知到ClassVisitor的相应方法(eg:解析到类方法时,就会回调ClassVisitor.visitMethod方法);

  3. 可以通过更改ClassVisitor中相应结构方法返回值,实现对类的代码切入(eg:更改ClassVisitor.visitMethod()方法的默认返回值MethodVisitor实例,通过操作该自定义MethodVisitor从而实现对原方法的改写);

  4. 其它的结构遍历也如同ClassVisitor

  5. 通过ClassWritertoByteArray()方法,得到class文件的字节码内容,最后通过文件流写入方式覆盖掉原先的内容,实现class文件的改写。

以上,就是 ASM Core Api 的整体运作流程。

接下来,我将对其中涉及到的重要的类进行详细解析。

ClassReader

ClassReader

这个类会提供你要转变的类的字节数组,它的accept方法,接受一个具体的ClassVisitor,并调用实现中具体的 visit,
visitSource, visitOuterClass, visitAnnotation, visitAttribute, visitInnerClass,visitField, visitMethodvisitEnd 方法。

ClassReader.accept(ClassVisitor classVisitor, int parsingOptions)中,第二个参数parsingOptions的取值有以下选项:

ClassWriter

ClassWriter

这个类是ClassVisitor的一个实现类,这个类中的toByteArray方法会将最终修改的字节码以 byte 数组形式返回。它可以单独使用,也可以传递给一个或多个ClassReaderClassVisitor适配器修改一个或多个已存在的Java类的类文件。

我们知道,类文件有着自己严格的格式,当我们想要注入相关代码时,不是直接注入相关指令就可以的,比如对于方法注入,我们可能还需要对栈帧图( stack map frames)进行计算:你需要计算所有的帧,找到有对象跳转或者绝对跳转的帧,最后还要压缩剩余的帧。同样,对于栈帧的局部变量表和操作数栈的大小也要自己进行计算。这些计算操作具备一定的难度,幸运的是,当我们创建一个ClassWriter时,可以配置 ASM 自动帮我们对指定的内容进行计算。具体的配置标识如下:

ClassWriter的构造函数需要传入一个 flag,其含义为:

使用这些标识很方便,但是会带来一些性能上的损失:COMPUTE_MAXS标识会使ClassWriter慢10%,COMPUTE_FRAMES标识会使ClassWriter慢2倍,

ClassVisitor

ClassVisitor

一个可以访问Java类的访问者。其方法被调用次序必须满足:

visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )* ( visitInnerClass | visitField | visitMethod )* visitEnd

visit必须第一个被调用,然后最多调用一次visitSource,同样接着最多调用一次visitOuterClass接下来按任意顺序可多次调用visitAnnotationvisitAttribute;接下来visitInnerClassvisitFieldvisitMethod同样按任意顺序可多次调用;最后调用一次visitEnd,表示类访问结束。

注: ASM 文档原文内容为:

This means that visit must be called first, followed by at most one call to visitSource, followed by at most one call to visitOuterClass, followed by any number of calls in any order to visitAnnotation and visitAttribute, followed by any number of calls in any order to visitInnerClass,visitField and visitMethod , and terminated by a single call to visitEnd.

黑体加粗句子我的翻译是:以任意顺序访问visitInnerClass,visitFieldvisitMethod,但是在我机器上试验得到的结果是这3者的访问顺序是固定的:visitInnerClass->visitField->visitMethod,所以,此处可能是翻译有问题,应该是以一定的顺序可多次调用 visitInnerClass,visitFieldvisitMethod。如有差错,烦请指正,感谢! ^-^

MethodVisitor

MethodVisitor

ASM 生成和转换class文件方法使用的是抽象类MethodVisitorClassVisitor.visitMethod方法返回的就是该实例。

其方法调用时序为:

visitAnnotationDefault?
( visitAnnotation | visitParameterAnnotation | visitAttribute )*
( visitCode
( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn |
visitLocalVariable | visitLineNumber )*
visitMaxs )?
visitEnd

即如果有annotations或者attributes,它们必须被第一个访问,接下来对于非抽象方法访问的就是方法内部字节码(visitCode),然后在visitCodevisitMaxs中的那些指令会按上面所示方法顺序访问,最后类方法访问结束回调visitEnd

class文件中,方法中的代码是以一系列的字节码指令组成的。如果要生成或者改变类内容,则需要先了解下这些指令的工作模型。

下面简单介绍指令的工作模型,了解这些内容就基本能够完成对类的一些简单的变换操作。如需更详细介绍,请参考 JVM 规范

局部变量表和操作数栈的大小决于方法代码,它们在编译时进行计算,并与类中的字节码指令一起存储。因此,对于同一个方法调用,所有帧的大小都是一样的,但是对于不同的方法调用,各个栈帧都拥有不同大小的局部变量表和操作数栈。


表 3.1 展示一个带有3个帧的运行栈样例。第一个帧包含3个局部变量,其操作数栈为4个字长大小,包含2个值。第二个帧包含2个局部变量和2个操作数值。第三个帧处于栈顶(当前帧),包含4个局部变量和2个操作数值。

当空栈压入一个帧时,其局部变量表会被初始化压入目标对象实例this(对于非静态方法)和方法参数变量。比如,调用a.equals(b)时,会创建一个帧,其局部变量表初始化有2个局部变量ab(其他变量为被初始化)。

package pkg;
public class Bean {
    private int f;
    public int getF() {
     return this.f;
}
  public void setF(int f) {
    this.f = f;
  }
}

上面代码的getF方法的字节码如下:

ALOAD 0
GETFIELD pkg/Bean f I
IRETURN

第一条指令读取局部变量表索引0的局部变量this,并将值压入到操作数栈中。第二条指令先获取操作数栈栈顶值this(弹出栈),获取该实例类成员f,并将其压入栈中。最后一条指令将操作数栈弹出,将值返回给调用者。具体过程如下图3.2 所示:

上面代码的setF(int f)方法的字节码如下:

ALOAD 0
ILOAD 1
PUTFIELD pkg/Bean f I
RETURN

第一条指令将局部变量表索引0的变量this压入到操作数栈;第二条指令将局部变量表索引1的变量f压入到操作数栈;第三条指令弹出这个值,并且将一个int值付给this.f;最后一条指令将当前栈帧销毁并将结果返回给调用者。具体过程如下图3.3 所示:

AnnotationVisitor

AnnotationVisitor

AnnotationVisitor api 访问时序如下:

( visit | visitEnum | visitAnnotation | visitArray )* visitEnd

Type

Type

Type对应的是 Java 类型,该类提供一些方法方便我们操控 Java 类型和描述符转换。
比如:

Notice

  1. 通过我们绑定ClassVisitorClassReader的代码如下:
byte[] b1 = ...; 
ClassWriter cw = new ClassWriter(0); 
// cv forwards all events to cw 
ClassVisitor cv = new ClassVisitor(ASM4, cw) { }; 
ClassReader cr = new ClassReader(b1); 
cr.accept(cv, 0);
 byte[] b2 = cw.toByteArray(); // b2 represents the same class as b1 

假设我们并不想做出改动类本身行为,那么按照上面的代码,效率会比较低,因为必须解析字节数组并且要经历事件循环;如果可以直接复制原本的字节数组b1b1,那么效率将大大提升。幸运的是,ASM 已考虑到这种情况,并为我们提供了优化方法,如下所示:

byte[] b1 = ... 
ClassReader cr = new ClassReader(b1); 
ClassWriter cw = new ClassWriter(cr, 0); //pass cr to cw directly
ChangeVersionAdapter ca = new ChangeVersionAdapter(cw); 
cr.accept(ca, 0);
 byte[] b2 = cw.toByteArray(); 

原理如下:

使用优化方法,性能上比前者提升有2倍多速度。需要注意的是,这种优化方法需要复制类中所有常量到新的字节数组中,这种优化对于增加成员,方法和指令来说,是没有问题的,但是对于删除或者更改类元素名称来说,会大大增加类文件大小,因此,建议对于 增加 动作的转换使用优化方法。

参考

JVM中的栈和局部变量
Jvm系列2—字节码指令

上一篇下一篇

猜你喜欢

热点阅读