深入浅出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

image

我们可以发现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流程的转发
image

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

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

尾声

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容