字节码插桩技术---ASM的使用(一)
字节码插桩技术---Android项目实操(二)
字节码插桩技术---Transform配合ASM进行插桩(三)
字节码插桩技术简单来讲就是通过操作class文件的字节码,对class文件进行改造。在很多三方框架都有应用,比如路由框架ARouter,热修复框架Robust等。而字节码插桩一般都会使用ASM工具进行操作,这篇文章会介绍ASM的简单使用,下篇博客会详细介绍,Android项目中如何进行字节码插桩
一、ASM的基本使用
ASM的官方地址
先创建一个原文件ASMTest.java
public class ASMTest {
public void test(){
}
}
编译之后会得到一个class文件,我们将这个class文件保存在相应的路径中,我们之后将会对这个class文件进行操作
之后创建一个目标文件,我们将使用ASM,对上面的原文件操作,效果就是和这个目标文件一样,如下:
public class ASMTest {
public void test(){
System.out.println("ASMTest=====>test");
}
}
使用javap或者ASMPlugin插件,获取这个类的字节码,我们将根据字节码指令,去写代码,字节码指令如下:
public class org.example.ASMTest {
public org.example.ASMTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void test();
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String ASMTest=====>test
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
由于只多了一行操作,所以很好确定将要操作的字节码是哪里,就是上面test中的0,3,5标识的地方。
文件都已经准备好,开始准备ASM工具,首先在项目中依赖ASM的两个库:
implementation 'org.ow2.asm:asm:9.3'
implementation 'org.ow2.asm:asm-commons:9.3'
执行类如下:
import org.objectweb.asm.*;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.Method;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws IOException {
//被改造的原class文件
FileInputStream fis = new FileInputStream("/Projects/Java/ASMTest/src/main/java/org/example/clazz/ASMTest.class");
ClassReader classReader = new ClassReader(fis);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM7,classWriter) {
//每个类的方法,都回调到这里
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor visitor = super.visitMethod(access, name, descriptor, signature, exceptions);
return new MyMethodVisitor(Opcodes.ASM7,visitor,access,name,descriptor);
}
};
classReader.accept(classVisitor,0);
byte[] bytes = classWriter.toByteArray();
//改造后的class文件
FileOutputStream fos = new FileOutputStream("/Projects/Java/ASMTest/src/main/java/org/example/clazz/ASMTest2.class");
fos.write(bytes);
fos.close();
}
static class MyMethodVisitor extends AdviceAdapter{
protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
}
@Override
protected void onMethodEnter() {
super.onMethodEnter();
Type type1 = Type.getType("Ljava/lang/System;");
Type type2 = Type.getType("Ljava/io/PrintStream;");
//对应字节码指令getstatic,
getStatic(type1,"out",type2);
//对应字节码指令ldc
visitLdcInsn("ASMTest=====>test");
//对应字节码指令invokevirtual
invokeVirtual(Type.getType("Ljava/io/PrintStream"),new Method("println","(Ljava/lang/String;)V"));
}
}
}
比较关键的地方我写了注释,我就不解释了,因为这只是AMS的一小部分api,更多的api可以自己找相关的文档来熟悉了解
二、在指定的方法进行插桩
执行上面的代码后,class如下:
package org.example;
public class ASMTest {
public ASMTest() {
System.out.println("ASMTest=====>test");
}
public void test() {
System.out.println("ASMTest=====>test");
}
}
我们发现,除了test方法中有了我们想要的代码,在构造方法中也多了这段代码。当然,我们可以在visitMethod方法中进行名称的判断,找到我们想要的方法,拦截不想改造的方法。但是很多时候,我们在进行插桩的时候,方法名称千万种,使用名称判断的这种办法是不可行的,那怎么办呢?我们可以使用注解,也就是说如果某个方法有特定的注解,则进行插桩操作;反之,不进行插桩
我们先创建一个注解:
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD})
public @interface MyAnnotation {
}
然后给ASMTest.java中的test方法加上这个注解
public class ASMTest {
@MyAnnotation
public void test(){
}
}
我们对执行类中的静态类MyMethodVisitor进行改造,如下:
static class MyMethodVisitor extends AdviceAdapter{
boolean inject = false;
protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
if(descriptor.equals("Lorg/example/MyAnnotation;")){
inject = true;
}
return super.visitAnnotation(descriptor, visible);
}
@Override
protected void onMethodEnter() {
super.onMethodEnter();
if(inject) {
Type type1 = Type.getType("Ljava/lang/System;");
Type type2 = Type.getType("Ljava/io/PrintStream;");
//对应字节码指令getstatic,
getStatic(type1, "out", type2);
//对应字节码指令ldc
visitLdcInsn("ASMTest=====>test");
//对应字节码指令invokevirtual
invokeVirtual(Type.getType("Ljava/io/PrintStream;"), new Method("println", "(Ljava/lang/String;)V"));
}
}
}
我们可以看见有如下几个变化:
inject:这个变量用来控制是否进行插桩操作
visitAnnotation:根据方法名称,我们也可以猜出来,就是解析方法的注解,如果方法没有注解,则不执行。方法里,我们进行了注解签名的判断,如果是我们想要的注解,则修改inject为true
之后,我们再执行,会得到如下结果:
package org.example;
public class ASMTest {
public ASMTest() {
}
@MyAnnotation
public void test() {
System.out.println("ASMTest=====>test");
}
}
只有@MyAnnotation注解的方法,才进行了插桩