前言
在学习JVM这个系列文章中,已经讲解了JVM规范、Class文件格式以及如何阅读字节码。本篇将和大家一起学习字节码处理框架ASM的使用。以巩固对字节码的理解~加油
如果你对JVM、字节码、Class文件格式还没有概念的话,建议先看之前的文章,相信会收获更多哦
Android工程师学习JVM(二)-教你阅读Java字节码
简介
说到字节码操作,我们很自然地会想到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深入学习使用这部分后续会用单独的专题来写哈,加油~