ASM 关键接口 MethodVisitor

2020-05-26 17:01:41 浏览数 (1)

Label label = new Label() 这个语句中,label的作用是为了条件跳转,其实也可以理解成字节码指令的参数。 所以label必须对应一条字节码指令,通过visitLabel(label)来调用,并且visitLabel的调用必须紧跟随着label对象指定的指令。 如例子中,第一个label指向goto后,所以顺序必须是:mv.visitJumpInsn(Opcodes.GOTO, end);

当ASM的ClassReader读取到Method时就转入MethodVisitor接口处理。 方法的定义,以及方法中指令的定义都会通过MethodVisitor接口通知给程序。我们假设有下面这样的一个类:

下面是这个MethodVisitor接口的所有方法定义。本文只会介绍主要的方法,因此不会逐个对方法做依次介绍:

这些方法必须按照以下顺序调用(和MethodVisitor接口在Javadoc中指定的一些额外约束):

代码语言:javascript复制
visitAnnotationDefault?
( visitAnnotation | visitParameterAnnotation | visitAttribute )*
( visitCode
( visitTryCatchBlock | visitLabel | visitFrame | visitXxx Insn | visitLocalVariable | visitLineNumber ) *
visitMaxs )?
visitEnd

这意味着,如有注释和属性的话,则必须先访问,后面是非抽象方法的字节码。 对于这些方法,这些代码必须按顺序访问,在唯一一个‘visitCode’方法调用和唯一一个‘visitMaxs’方法调用之间。

该接口的方法数量如此之多,甚至是ClassVisitor接口的3倍以上。但是值得关心的接口只有下面这几个,其余的都是和代码有关系:

visitCode

ASM开始扫描这个方法。

visitMaxs(maxStack, maxLocals);

该方法是visitEnd之前调用的方法,可以反复调用。 用以确定类方法在执行时候的堆栈大小。

visitEnd();

表示方法输出完毕

visitCode 和 visitMaxs 方法可用于检测该方法的字节代码在一个事件序列中的 开始与结束。和类的情况一样,visitEnd 方法也必须在最后调用,用于检测一个方法在一个事件序列中的结束。

visitMethodInsn

代码语言:javascript复制
  /**
   * 访问方法的指令。 方法指令是调用方法的指令。
   *
   * @param opcode 要访问的类型指令的操作码。可以是INVOKEVIRTUAL,INVOKESPECIAL,INVOKESTATIC或INVOKEINTERFACE。
   * @param owner 方法的所有者类的内部名称 (see {@link
   *     Type#getInternalName()}).
   * @param name 方法名
   * @param descriptor the method's descriptor (see {@link Type}).
   * @param isInterface if the method's owner class is an interface.
   */
  public void visitMethodInsn(
      final int opcode,
      final String owner,
      final String name,
      final String descriptor,
      final boolean isInterface) {
    if (api < Opcodes.ASM5 && (opcode & Opcodes.SOURCE_DEPRECATED) == 0) {
      if (isInterface != (opcode == Opcodes.INVOKEINTERFACE)) {
        throw new UnsupportedOperationException("INVOKESPECIAL/STATIC on interfaces requires ASM5");
      }
      visitMethodInsn(opcode, owner, name, descriptor);
      return;
    }
    if (mv != null) {
      mv.visitMethodInsn(opcode & ~Opcodes.SOURCE_MASK, owner, name, descriptor, isInterface);
    }
  }

visitVarInsn

访问局部变量指令。 局部变量指令是加载loads或存储stores局部变量值的指令。

代码语言:javascript复制
 /**
   * @param opcode 要访问的局部变量指令的操作码。 该操作码是
   *     ILOAD, LLOAD, FLOAD, DLOAD, ALOAD, ISTORE, LSTORE, FSTORE, DSTORE, ASTORE or RET.
   * @param var 要访问的指令的操作数。该操作数是局部变量的索引。
   */
  public void visitVarInsn(final int opcode, final int var) {
    if (mv != null) {
      mv.visitVarInsn(opcode, var);
    }
  }

需要注意的是,没有必要为了开始访问另外一个方法,而结束当前访问的方法。 实际上,‘MethodVisitor’实例间是完全独立的,可以用任何顺序调用(但必须在‘cv.visitEnd()’调用之前使用):

ASM提供了三个基于MethodVisitor API的核心组件,用于生成和转换方法:

ClassReader类解析一个编译后的方法,并且通过传递ClassVisitor作为accept方法的参数获得的返回,调用MethodVisitor’相应的方法。 ClassWriter的‘visitMethod’返回了MethodVisitor抽象类的一个实现,该实现可以直接用二进制的方式构建编译后的方法。 MethodVisitor类可以传递所有调用它的方法给另一个MethodVisitor类。MethodVisitor类可以看作一个事件过滤器。

实现类 - MethodWriter

生成相应的“ method_info”结构的MethodVisitor,如Java虚拟机规范(JVMS)中所定义。

visitMaxs

代码语言:javascript复制
  @Override
  public void visitMaxs(final int maxStack, final int maxLocals) {
    if (compute == COMPUTE_ALL_FRAMES) {
      computeAllFrames();
    } else if (compute == COMPUTE_MAX_STACK_AND_LOCAL) {
      computeMaxStackAndLocal();
    } else if (compute == COMPUTE_MAX_STACK_AND_LOCAL_FROM_FRAMES) {
      this.maxStack = maxRelativeStackSize;
    } else {
      this.maxStack = maxStack;
      this.maxLocals = maxLocals;
    }
  }

AnalyzerAdapter实现类

一个MethodVisitor,用于跟踪visitFrame调用之间的 stack map frame 更改。 该适配器必须与ClassReader.EXPAND_FRAMES选项一起使用。 每个visitX指令都将委托给链中的下一个访问者(如果有),然后模拟该指令对 stack map frame(由局部变量和堆栈表示)的影响。 链中的下一个访问者可以通过读取其visitX方法中的这些字段的值来获取每条指令之前的 stack map frame 的状态(这需要引用链中位于其之前的AnalyzerAdapter)。 如果此适配器与不包含堆栈映射表属性的类一起使用(即Java 6之前的类),则此适配器可能无法为每条指令计算堆栈映射框架。 在这种情况下,不会抛出异常,但是对于这些指令,locals和stack字段将为null。

这个方法适配器会根据 visitFrame 方法中被访问的帧,计算出每一个指令之前的栈哈希帧。

为了节省空间,visitFrame仅仅会在一个方法中某些特定的指令前调用,并且“其他的帧也可以从这些帧简单容易的推算出来”。这就是AnalyzerAdapter的作用。

当然在它仅能作用在包含了预先计算过栈哈希帧的编译类,即使用Java 6或者更改版本编译的类(或者像之前的示例一样,使用含有COMPUTE_FRAMES参数的ASM adapter将类升级到Java 6):

‘stack’属性在AnalyzerAdapter类中有定义,并且包含了在操作栈中的类型。 更确切地说,对于一个‘visitXxx Insn’指令,在覆盖方法被调用前,这个列表包含了在该条指令前操作栈的状态。 需要注意的是覆盖方法必须被调用,这样栈里的属性才能正确地更新(因此使用父类的原始方法,而不是mv的方法)。 另外,调用父类的方法也可以插入新指令:效果是AyalyzerAdapter会计算出这些指令对应的帧。 因此,该适配器会基于它计算出的帧更新visitMaxs方法的参数,我们就不必更新这些参数了:

0 人点赞