Android工程师学习JVM(三)-字节码框架ASM使用

1,261 阅读4分钟

前言

在学习JVM这个系列文章中,已经讲解了JVM规范、Class文件格式以及如何阅读字节码。本篇将和大家一起学习字节码处理框架ASM的使用。以巩固对字节码的理解~加油

如果你对JVM、字节码、Class文件格式还没有概念的话,建议先看之前的文章,相信会收获更多哦

Android工程师学习JVM(二)-教你阅读Java字节码

Android工程师学习JVM(一)-JVM概述

简介

说到字节码操作,我们很自然地会想到APT、Javassist、Java动态代理、CgLib、AspectJ、ASM等框架。但在这些框架中ASM相对比较底层,也因此理论上它可以实现任何关于字节码的修改,非常硬核。许多字节码生成API底层都是用ASM实现,比如刚提到的CgLib、Android中常用的Groovy。如果想要把ASM学好,则必须要深入学习JVM。

1、为什么说ASM很底层

Java字节码是严格按照JVM规范生成的二进制字节流。而ASM是按照这个规范将java字节码转换成java中的对象,并按照规范定制了一系列对字节码进行操作的API。因此ASM和Java字节码规范之间是有很直接的对应关系的。

比如,下面这行代码

System.out.println("restart Android");

通过javap -v xxx.class命令查看这行代码对应的JVM汇编指令

Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String restart Android
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return

使用ASM工具ASMifier将该行代码转换为API(Core API):

mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("restart Android");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(RETURN);

眼尖的同学到这里应该已经明显感受到了,ASM的API和JVM的汇编指令非常接近。因此学好JVM对学习ASM非常有帮助。

2、ASM API

2.1、ASM编程模型

Core API:提供了基于事件形式的编程模型。该模型不需要一次性将整个类的结构读取到内存中,因此这种方式更快,需要更少的内存。但这种编程方式难度更大。

Tree API: 提供了基于树形的编程模型。该模型需要一次性将一个类的完整结构全部读取到内存中,所以这种方式需要更多的内存。但这种编程方式难度较低。

下面我们用个案例讲述Core API模型

案例:

原文件:

public class Restart {
    public void m1() {
        System.out.println("restart Android");
    }
}

通过字节码操作增强,增加计算方法时间的操作

public class Restart {
    public Restart() {
    }

    public void m1() {
        TimeLogger.start();
        System.out.println("restart Android");
        TimeLogger.end();
    }
}

这个案例实际上也是个简单的AOP操作了哈。

TimeLogger类代码:

public class TimeLogger {

    private static long a1 = 0;

    public static void start() {
        a1 = System.currentTimeMillis();
    }

    public static void end() {
        long a2 = System.currentTimeMillis();
        System.out.println("now invoke method use time == " + (a2 -  a1));
    }

}

2.2、Core API

在开始之前,让我们先来了解一下ASM Core API的调用流程:

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

2、ASM在ClassReader解析class文件过程中,解析到某一个结构就会通知到ClassVisitor的响应方法。如解析到方法时,就会回调ClassVisitor.visitMethod方法

3、通过更改ClassVisitor中对应结构方法的返回值,实现对类的修改。如修改ClassVisitor.visitMethod方法的返回值MethodVisitor实例,实现对方法的改写

4、使用ClassWriter的toByteArray()方法,得到修改后的class文件的字节码内容,再通过文件IO对原class文件进行覆盖

按照以上步骤,可实现代码如下:

public class CoreTest {

    public static void main(String[] args) throws IOException {
        //使用ClassReader读取class
        ClassReader cr = new ClassReader("com/restart/asm/Restart");
        //创建ClassWriter,注意ClassWriter是继承ClassVisitor的
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        //将ClassWriter传入自定义的ClassVisitor,执行自定义的修改
        ClassVisitor cv = new ClassTimeVisitor(cw);
        //ClassReader传入ClassVisitor,在读取过程中触发各个事件,交由ClassVisitor消费
        cr.accept(cv, ClassReader.SKIP_DEBUG);
        //获取修改后的字节码
        byte[] data = cw.toByteArray();
        //将修改后的字节码覆盖原文件
        File file = new File("build/classes/java/main/com/restart/asm/Restart.class");
        System.out.println(file.getAbsoluteFile());
        FileOutputStream fos = new FileOutputStream(file);
        fos.write(data);
        fos.close();
    }

}

ClassTimeVisitor代码如下:

public class ClassTimeVisitor extends ClassVisitor {

    public ClassTimeVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM7, classVisitor);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        cv.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
        if (!"<init>".equals(name) && mv != null) {
            //非初始化方法就增加记录执行时间
            mv = new MethodTimeVisitor(mv);
        }
        return mv;
    }
}

MethodVisitor代码如下:

public class MethodTimeVisitor extends MethodVisitor {

    public MethodTimeVisitor(MethodVisitor mv) {
        super(Opcodes.ASM7, mv);
    }

    @Override
    public void visitCode() {
        //在方法入口处增加操作
        mv.visitCode();
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/restart/core/TimeLogger", "start", "()V", false);
    }

    @Override
    public void visitInsn(int opcode) {
		//在Return之前添加代码
        if (opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN || opcode == Opcodes.ATHROW) {
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/restart/core/TimeLogger", "end", "()V", false);
        }

        mv.visitInsn(opcode);
    }
}

3、小结

1、学习ASM框架使用需要对JVM字节码有一定的掌握,因为ASM的API和Java字节码规范很类似

2、ASM编程模型有两种,一类是CoreAPI,一类是TreeAPI,这个很像xml解析的sax模型和dom模型

3、ASM的Core API基本使用流程: ClassReader读取字节码产生事件、ClassVisitor消费事件,ClassWriter也是一个ClassVisitor

4、对ASM的API深入学习使用这部分后续会用单独的专题来写哈,加油~