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