阅读 1200

ASM深入浅出

前言

ASM作为一个声名在外的字节码编制工具,无数“传奇”框架都基于此展现了花里胡哨的魔法。

最近在工作中发现需要加强这部分能力,不然很多技术方案总是很麻烦...但是仅靠ASM实际也无法“无所欲为”,因为说到底它也只是一个方便的改写class的工具。想要使其发挥战斗力,还需要配合诸如:Gradle的transform api、注解等角色的支持。

因此接下来的一段时间内,我会尽可能的把自己在这方面的实战内容输出出来。

正文

这一篇咱们主要聊ASM的一些用法,核心聚焦于ASM。所以关于字节码的部分就不展开了,有相关兴趣的同学可以自行了解吧

说实话ASM整体使用起来很简单,主要有数个核心类:ClassReaderClassWriteClassVisitor...等各种Visitor系列类。

从类名的定义上,我们可以猜到ClassReader用于读取class文件;ClassWrite用于改写class文件。而ClassVisitorFieldVisitor...则抽象类、方法、对象访问的流程。

第一步先把依赖加上,AMS现在已经出到很高的版本了,不过咱们还是随便用个版本,反正够用~

implementation 'org.ow2.asm:asm:6.0'
implementation 'org.ow2.asm:asm-util:6.0'
复制代码

一、读Class

下边看一个简单的读class的demo代码:

fun main() {
    val cp = ClassPrinter()
    val cr = ClassReader("com.test.asm.AsmDemo")
    cr.accept(cp, 0)
}

class AsmDemo {
    private val hello = "Hello ASM"

    fun testAsm() {
        invokeMethod()
    }

    private fun invokeMethod() {
        print(hello)
    }
}


class ClassPrinter : ClassVisitor(ASM6) {
    override fun visit(
        version: Int, access: Int, name: String?, signature: String?, superName: String?, interfaces: Array<String>
    ) {
        println("$name extends $superName {")
    }
	// 为了看起来简单,移除了一些不是特别重要的方法

    override fun visitField(access: Int, name: String?, desc: String?, signature: String?, value: Any?): FieldVisitor? {
        println("    visitField(name:$name desc:$desc signature:$signature)")
        return null
    }

    override fun visitMethod(access: Int, name: String?, desc: String?, signature: String?, exceptions: Array<String>?): MethodVisitor? {
        println("    visitMethod(name:$name desc:$desc signature:$signature)")
        return null
    }

    override fun visitEnd() {
        println("}")
    }
}
复制代码

这段代码run起来之后输出了如下的信息:

com/test/asm/AsmDemo extends java/lang/Object {
    visitField(name:hello desc:Ljava/lang/String; signature:null)
    visitMethod(name:testAsm desc:()V signature:null)
    visitMethod(name:invokeMethod desc:()V signature:null)
    visitMethod(name:<init> desc:()V signature:null)
}
复制代码

从demo中我们看到通过我们通过ClassReader("com.test.asm.AsmDemo"),读取对应的Class。而ClassPrinter : ClassVisitor(ASM6)通过对应的visit方法来了解对应类的细节。

二、写Class

对于写的过程相对要复杂一些,毕竟操作空间比较大。比如下边这个移除某方法的操作:

fun main() {
    val cr = ClassReader("com.test.asm.AsmDemo")
    val cw = ClassWriter(cr, 0)
    val adapter = RemoveMethodAdapter(cw, "testAsm")
    cr.accept(adapter, 0)
    // 输出class
    val outFile = File("...本地地址/com/test/asm/TestAsmDemo.class")
    outFile.writeBytes(cw.toByteArray())
}

public class RemoveMethodAdapter extends ClassVisitor {
      private String mName;
      private String mDesc;
      public RemoveMethodAdapter( ClassVisitor cv, String mName, String mDesc) {
          super(ASM7, cv);
          this.mName = mName;
		  this.mDesc = mDesc;
      }
      @Override
      public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
          if (name.equals(mName) && desc.equals(mDesc)) {
              // 不要委托至下一个访问器 -> 这样将移除该方
              return null;
		  }
          return cv.visitMethod(access, name, desc, signature, exceptions);
      }
}
复制代码

打开输出的TestAsmDemo.class

我们可以发现testAsm()方法已经在ClassWriter这个实例中被移除了。

三、源码速读

整个流程的开始,就在于ClassReader的accept()方法:

public void accept(
      final ClassVisitor classVisitor,
      final Attribute[] attributePrototypes,
      final int parsingOptions) {
 	// 省略大量代码     
    if ((parsingOptions & SKIP_DEBUG) == 0
        && (sourceFile != null || sourceDebugExtension != null)) {
      classVisitor.visitSource(sourceFile, sourceDebugExtension);
    }
}
复制代码

上述简单截取了一段代码,这里本质就是读取class文件,然后按class的规范去解析,然后回调ClassVisitor对应的接口方法。

对于ClassVisitor的实现来说,可以是我们自己的实现类,这样我们就可以访问到ClassReader解析class的过程。

但是一般来说我们需要去改写class,此外核心的类便是ClassWriter。

public class ClassWriter extends ClassVisitor {
  // 省略大量代码
  @Override
  public final MethodVisitor visitMethod(
      final int access,
      final String name,
      final String descriptor,
      final String signature,
      final String[] exceptions) {
    MethodWriter methodWriter =
        new MethodWriter(symbolTable, access, name, descriptor, signature, exceptions, compute);
    if (firstMethod == null) {
      firstMethod = methodWriter;
    } else {
      lastMethod.mv = methodWriter;
    }
    return lastMethod = methodWriter;
  }
}
复制代码

这里我们单独截取了visitMethod()方法,可以看到这里真正的实现是通过MethodWriter实现的:

final class MethodWriter extends MethodVisitor {
  // 省略大量代码
  @Override
  public void visitMethodInsn(
      final int opcode,
      final String owner,
      final String name,
      final String descriptor,
      final boolean isInterface) {
    lastBytecodeOffset = code.length;
    Symbol methodrefSymbol = symbolTable.addConstantMethodref(owner, name, descriptor, isInterface);
    if (opcode == Opcodes.INVOKEINTERFACE) {
      code.put12(Opcodes.INVOKEINTERFACE, methodrefSymbol.index)
          .put11(methodrefSymbol.getArgumentsAndReturnSizes() >> 2, 0);
    } else {
      code.put12(opcode, methodrefSymbol.index);
    }
    // 省略部分代码
  }
}
复制代码

可以看到,这里封装了写class的代码。走到这我们就能明白:我们之所以能够做到改写class的效果,本质是因为ClassWriter这个类的封装,而这个类是基于ClassVisitor的访问者模式来了解到ClassReader加载解析class的过程。

更多的咱们就不看了,大家有兴趣自己跟一波吧。毕竟以上的代码就足以让我们理解ASM整体的工作流程。

四、小总结

结合上述的内容,咱们来一个总结

首先ASM整体基于访问者模式(不了解这个模式也没关系,不影响理解)。

  • ClassReader解析class文件,并回调对应ClassVisitor接口的方法
    • ClassReader只负责分析class文件,然后回调给ClassVisitor,至此ClassReader也就结束了它的工作
  • ClassWriter 是ClassVisitor接口的实现
    • 这里封装了对class文件写的操作
      • 具体代码在ClassWriter里边的各种Writer实现。
    • ClassWriter也是一个ClassVisitor
      • 它是咱们的第一层“代理”,我们的自定义ClassVisitor通过传入ClassWriter,来做到visitor流程的转发

因此,可以这么说:ClassReader + ClassVisitor 采用标准的访问者模式。目的在于:ASM框架基于我们一套接口,可以让我们访问到一个class文件的各种流程。

当我们需要改写class时候,我们则需要ClassWriter这个特别的ClassVisitor来进行能力的增强。

尾声

本篇内容很短,但也算是拉开“幕后”编改字节码的序幕。

后面的文章会一步步的走入真正的应用中去。相关知识涉及面众多,我会尽可能的用通俗的方式将这部分内容展现给大家看。

更多更新的文章欢迎关注我们的公众号:咸鱼正翻身

文章分类
Android
文章标签