Android ASM插桩的初步使用(一)
今天看了插桩,主要还是对ASM的初步使用。废话也不多说,介绍网上都有,直接上干货。
前面介绍部分 摘抄了不少
奶盖ww 的 成为架构师的路上,你究竟了解多少字节码插桩?
大家可以看一看,我也不太懂。
ASM插桩流程
- 需要创建一个 ClassReader 对象,将 .class 文件的内容读入到一个字节数组中
- 然后需要一个 ClassWriter 的对象将操作之后的字节码的字节数组回写
- 需要事件过滤器 ClassVisitor。在调用 ClassVisitor 的某些方法时会产生一个新的 XXXVisitor 对象
- 当我们需要修改对应的内容时只要实现自己的 XXXVisitor 并返回就可以了
ClassReader 类
这个类会将 .class 文件读入到 ClassReader 中的字节数组中,它的 accept 方法接受一个 ClassVisitor 实现类,并按照顺序调用 ClassVisitor 中的方法
ClassWriter 类
ClassWriter 是一个 ClassVisitor 的子类,是和 ClassReader 对应的类,ClassReader 是将 .class 文件读入到一个字节数组中,ClassWriter 是将修改后的类的字节码内容以字节数组的形式输出。
ClassVisitor 抽象类
void visit(int version, int access, String name, String signature, String superName, String[] interfaces)
该方法是当扫描类时第一个调用的方法,主要用于类声明使用。下面是对方法中各个参数的示意:visit( 类版本 , 修饰符 , 类名 , 泛型信息 , 继承的父类 , 实现的接口)* AnnotationVisitor visitAnnotation(String desc, boolean visible)
该方法是当扫描器扫描到类注解声明时进行调用。
下面是对方法中各个参数的示意:
- visitAnnotation(注解类型 , 注解是否可以在 JVM 中可见)。
- FieldVisitor visitField(int access, String name, String desc, String signature, Object value)该方法是当扫描器扫描到类中字段时进行调用。下面是对方法中各个参数的示意:visitField(修饰符 , 字段名 , 字段类型 , 泛型描述 , 默认值)
- MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions)
该方法是当扫描器扫描到类的方法时进行调用。下面是对方法中各个参数的示意:visitMethod(修饰符 , 方法名 , 方法签名 , 泛型信息 , 抛出的异常) - void visitEnd()
该方法是当扫描器完成类扫描时才会调用,如果想在类中追加某些方法
MethodVisitor & AdviceAdapter
MethodVisitor 是一个抽象类,当 ASM 的 ClassReader 读取到 Method 时就转入 MethodVisitor 接口处理。
AdviceAdapter 是 MethodVisitor 的子类,使用 AdviceAdapter 可以更方便的修改方法的字节码。
AdviceAdapter
其中比较重要的几个方法如下:
void visitCode():表示 ASM 开始扫描这个方法
void onMethodEnter():进入这个方法
void onMethodExit():即将从这个方法出去
void onVisitEnd():表示方法扫码完毕
引用
首先,不用多想,肯定先上gradle插件引用啦
testImplementation 'org.ow2.asm:asm:7.1'
testImplementation 'org.ow2.asm:asm-commons:7.1'
由于本身使用asm,上面的包肯定是要导进来的,下面的主要还是对AdciceAdapter的使用。比传统自带的MethodVisitor好用些,我也没用过MethodVisitor,可能后面看看,先用AdciceAdapter吧。
写一个java文件
这个不多说,直接写一个java最简单的sout
public static void main(String[] var0) throws InterruptedException {
long s1 = System.currentTimeMillis();
Thread.sleep(1000L);
long s2 = System.currentTimeMillis();
System.out.println(s2 - s1);
}
运行下 得到了.class文件。
很nice。下面就开始ASM字节码编写了
推荐个插件
这个直接在AS的插件就能找到,方便开发。
对上面的进行插件转化得到下面的代码:
public class com/example/asmtest/MyClass {
// compiled from: MyClass.java
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/example/asmtest/MyClass; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V throws java/lang/InterruptedException
L0
LINENUMBER 6 L0
INVOKESTATIC java/lang/System.currentTimeMillis ()J
LSTORE 1
L1
LINENUMBER 7 L1
LDC 1000
INVOKESTATIC java/lang/Thread.sleep (J)V
L2
LINENUMBER 8 L2
INVOKESTATIC java/lang/System.currentTimeMillis ()J
LSTORE 3
L3
LINENUMBER 9 L3
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LLOAD 3
LLOAD 1
LSUB
INVOKEVIRTUAL java/io/PrintStream.println (J)V
L4
LINENUMBER 10 L4
RETURN
L5
LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
LOCALVARIABLE s1 J L1 L5 1
LOCALVARIABLE s2 J L3 L5 3
MAXSTACK = 5
MAXLOCALS = 5
}
准备完毕,开始写代码了。
开始
步骤一
首先要把.class文件加载进来
将插桩结果写在另一个class文件
try {
FileInputStream fis = new FileInputStream("D:\\Example\\ASMTest\\app\\src\\test\\java\\com\\example\\asmtest\\MyClass.class");
ClassReader classReader = new ClassReader(fis);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
classReader.accept(new MyClassVisitor(7 << 16 | 0 << 8, classWriter), ClassReader.EXPAND_FRAMES);
byte[] bytes = classWriter.toByteArray();
FileOutputStream fos = new FileOutputStream("D:\\Example\\ASMTest\\app\\src\\test\\java\\com\\example\\asmtest\\MyClass1.class");
fos.write(bytes);
fos.close();
fis.close();
} catch (Exception e) {
e.printStackTrace();
}
这个就是我将\MyClass.class读进来然后写在\MyClass1.class文件中。
MyClassVisitor
这个类是我自己继承ClassVisitor写的自定义类
@Override
public MethodVisitor visitMethod(int i, String s, String s1, String s2, String[] strings) {
MethodVisitor methodVisitor = super.visitMethod(i, s, s1, s2, strings);
System.out.println(s);
return new MyMethod(api, methodVisitor, i, s, s1);
}
主要重写了visitMethod方法, 返回调用MyMethod类。
MyMethod
如前文所说,onMethodEnter()和onMethodExit(int opcode)方法十分重要。我们重要重写这个方法。
public class MyMethod extends AdviceAdapter {
protected MyMethod(int i, MethodVisitor methodVisitor, int i1, String s, String s1) {
super(i, methodVisitor, i1, s, s1);
}
int s;
@Override
protected void onMethodEnter() {
super.onMethodEnter();
invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
s = newLocal(Type.LONG_TYPE);
storeLocal(s);
}
int s2;
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
super.endMethod();
invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
s2 = newLocal(Type.LONG_TYPE);
storeLocal(s2);
/*
* GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LLOAD 3
LLOAD 1
LSUB
INVOKEVIRTUAL java/io/PrintStream.println (J)V
* */
getStatic(Type.getType("Ljava/lang/System;"), "out", Type.getType("Ljava/io/PrintStream;"));
loadLocal(s);
loadLocal(s2);
math(SUB, Type.LONG_TYPE);
invokeVirtual(Type.getType("Ljava/io/PrintStream;"), new Method("println", "(J)V"));
}
这个代码是怎么出来的呢,主要还是依靠上面的插件啦!
这样,依靠这个就写出来了,对了,附上方法签名表
引用的地址附上 https://www.jianshu.com/p/44d3c7d46e5f
验证
我们看看结果,把.java文件的main方法只留一个Thread.sleep(1000L);
得到.class文
public class MyClass {
public MyClass() {
}
public static void main(String[] args) throws InterruptedException {
Thread.sleep(1000L);
}
}
运行代码:
OK成功啦,自动生成了!入门就这样。现在是都没有任何的过滤,所有的全部都加上这个语句,那么如果想要进行添加的话,可以自定义添加注解,通过注解再进行过滤。
ASM初步使用(二)
可以看看我写的代码:https://github.com/wcz201/ASMTest