preface

In the Learning THE JVM series, you’ve covered the JVM specification, the Class file format, and how to read bytecode. In this article, you will learn about the use of ASM, a bytecode processing framework. To reinforce our understanding of bytecode

If you are not familiar with the JVM, bytecode, or Class file format, you are advised to read the previous article

Android Engineer learning JVM(II)- Teaches you to read Java bytecode

Android Engineers learn about JVM(I)- Overview of the JVM

Introduction to the

When we think of bytecode manipulation, we naturally think of APT, Javassist, Java dynamic Proxy, CgLib, AspectJ, ASM, and other frameworks. But ASM is relatively low-level in these frameworks, so it can theoretically implement any bytecode modification, very hardcore. Many bytecode generation apis, such as CgLib and Groovy commonly used in Android, are implemented using ASM at the bottom. If you want to learn ASM well, you must learn more about the JVM.

1. Why is ASM very low-level

Java bytecode is a binary stream of bytes generated strictly according to the JVM specification. ASM translates Java bytecodes into Objects in Java according to the specification and customizes a set of apis for manipulating bytecodes according to the specification. Thus there is a direct correspondence between ASM and the Java bytecode specification.

For example, the following line of code

System.out.println("restart Android");
Copy the code

Use the javap -v xxx.class command to view the JVM assembly instructions for this line of code

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
Copy the code

Use the ASM tool ASMifier to convert this line of code into a 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);
Copy the code

For sharp-eyed students, it should be obvious by now that the ASM API is very close to the JVM’s assembly instructions. So learning the JVM is very helpful for learning ASM.

2, ASM API

2.1. ASM programming model

Core API: Provides a programming model based on event form. This model does not require the entire class structure to be read into memory at once, so this approach is faster and requires less memory. But programming this way is more difficult.

Tree API: Provides a tree-based programming model. This model requires that the entire structure of a class be read into memory at once, so this approach requires more memory. But this approach to programming is less difficult.

Let’s use an example to illustrate the Core API model

Case study:

The original file:

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

An operation that increases the time of a calculation method, enhanced by bytecode manipulation

public class Restart {
    public Restart(a) {}public void m1(a) {
        TimeLogger.start();
        System.out.println("restart Android"); TimeLogger.end(); }}Copy the code

This case is actually a simple AOP operation.

TimeLogger class:

public class TimeLogger {

    private static long a1 = 0;

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

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

2.2, the Core API

Before we start, let’s take a look at the ASM Core API call flow:

ASM provides a class reader that lets you easily read and parse class files.

2. When a structure is parsed by a ClassReader, ASM notifies the response method of the ClassVisitor. If a method is resolved, the classVisitor.visitMethod method is called back

3. Modify the class by changing the return value of the corresponding structure method in the ClassVisitor. For example, modify the return value of the classVisitor.visitMethod method instance to implement a rewrite of the method

4. Use the toByteArray() method of ClassWriter to obtain the bytecode content of the modified class file, and then use file IO to overwrite the original class file

According to the above steps, the code can be implemented as follows:

public class CoreTest {

    public static void main(String[] args) throws IOException {
        // Use ClassReader to read the class
        ClassReader cr = new ClassReader("com/restart/asm/Restart");
        // Create ClassWriter. Note that ClassWriter extends from ClassVisitor
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        // Pass the ClassWriter to the custom ClassVisitor to perform the custom modification
        ClassVisitor cv = new ClassTimeVisitor(cw);
        // The ClassReader is passed to the ClassVisitor, and events are triggered during the reading process, which is consumed by the ClassVisitor
        cr.accept(cv, ClassReader.SKIP_DEBUG);
        // Get the modified bytecode
        byte[] data = cw.toByteArray();
        // Overwrite the original file with the modified bytecode
        File file = new File("build/classes/java/main/com/restart/asm/Restart.class");
        System.out.println(file.getAbsoluteFile());
        FileOutputStream fos = newFileOutputStream(file); fos.write(data); fos.close(); }}Copy the code

ClassTimeVisitor code is as follows:

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) {
            // Non-initialization methods increase the record execution time
            mv = new MethodTimeVisitor(mv);
        }
        returnmv; }}Copy the code

MethodVisitor code is as follows:

public class MethodTimeVisitor extends MethodVisitor {

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

    @Override
    public void visitCode(a) {
        // Add actions to the method entry
        mv.visitCode();
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/restart/core/TimeLogger"."start"."()V".false);
    }

    @Override
    public void visitInsn(int opcode) {
		// Add code before 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); }}Copy the code

3, summary

Learning to use the ASM framework requires some familiarity with JVM bytecode, as the ASM API is similar to the Java bytecode specification

2. There are two ASM programming models, one is CoreAPI, the other is TreeAPI, which is much like the SAX model and DOM model for XML parsing

3, ASM Core API basic use process: ClassReader reads bytecode event, ClassVisitor consumes event, ClassWriter is also a ClassVisitor

4. Learn more about the ASM API and use it in a separate section