ASM概述
- ASM是一个功能比较齐全的java字节码操作与分析框架,通过ASM框架,我们可以动态的生成类或者增强已有类的功能。
- ASM可以直接生成二进制.class文件,也可以在类被加载入java虚拟机之前动态改变现有类的行为。
- java的二进制文件被存储在严格格式定义的.class文件里,这些字节码文件拥有足够的元数据信息用来表示类中的所有元素,包括类名称、方法、属性以及java字节码指令。ASM从字节码文件读入这些信息后,能够改变类行为、分析类的信息,甚至还可以根据具体的要求生成新的类。
- ASM 通过树这种数据结构来表示复杂的字节码结构,因为需要处理字节码结构是固定的,所以可以利用Visitor(访问者) 设计模式来对树进行遍历,在遍历过程中对字节码进行修改。
我们能用ASM来做什么
(1):对现有的类进行分析,可做代码、权限检查
(2):生成新的Class文件
(3):对已有的类进行转换
![](https://img.haomeiwen.com/i8669504/5f92835bfaa290e4.png)
字节码插桩在应用层能做什么?
- 函数耗时监听
- 无埋点框架
- 隐私合规检测
- 安装包防破解
查看文件字节码
1.在Plugins中搜索 ASM Bytecode Viewer,然后Restart。
![](https://img.haomeiwen.com/i8669504/39ce5aa50b0f052f.png)
2.编译之后,打开编译后的.class文件,使用ASM Bytecode Viewer命令。
![](https://img.haomeiwen.com/i8669504/2b85dedddc14821a.png)
3.生成字节码文件。
![](https://img.haomeiwen.com/i8669504/b813904b07ea4090.png)
ASM Core API
![](https://img.haomeiwen.com/i8669504/9592f7621a24dd4a.png)
-
ClassReader:这个类会将 .class 文件读入到 ClassReader 中的字节数组中,它的 accept 方法接受一个 ClassVisitor 实现类,并按照顺序调用 ClassVisitor 中的方法
-
ClassVisitor:主要负责访问类的成员信息。包括标记在类上的注解、类的构造方法、类的字段、类的方法、静态代码块等
-
ClassWriter:ClassWriter 是一个 ClassVisitor 的子类,是和 ClassReader 对应的类,ClassReader 是将 .class 文件读入到一个字节数组中,ClassWriter 是将修改后的类的字节码内容以字节数组的形式输出。
-
AdviceAdapter:MethodVisitor 是一个抽象类,当 ASM 的 ClassReader 读取到 Method 时就转入 MethodVisitor 接口处理。AdviceAdapter 是 MethodVisitor 的子类,封装了一些常用的方法,方便我们使用。我们重写其中的进入方法和退出方法的方法,并在其中加入我们要插入的字节码。AdviceAdapter其中几个重要方法如下:
void visitCode():表示 ASM 开始扫描这个方法
void onMethodEnter():进入这个方法
void onMethodExit():即将从这个方法出去
void onVisitEnd():表示方法扫描完毕
关键类ClassVisitor
![](https://img.haomeiwen.com/i8669504/558f41f198171dd2.png)
-
visit
/**
* 可以拿到类的详细信息
*
* @param version jdk的版本: 52 代表jdk版本 1.8;51 代表jdk版本 1.7
* @param access 类的修饰符:ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED、ACC_FINAL、ACC_SUPER
* @param name 类的名称:以路径的形式表示 com/joker/demo/TestClass
* @param signature 泛型信息:未定义泛型,则该参数为null
* @param superName 表示当前类所继承的父类
* @param interfaces 表示类所实现的接口列表
*/
@Override
void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces)
}
-
visitAnnotation
/**
* 当扫描器扫描到类注解声明时进行调用
*
* @param desc 注解类型(签名类型)
* @param visible 注解是否可以在 JVM 中可见
* @return
*/
@Override
AnnotationVisitor visitAnnotation(String desc, boolean visible) {
return super.visitAnnotation(desc, visible)
}
-
visitField
/**
* 当扫描器扫描到类中字段时进行调用
*
* @param access 修饰符
* @param name 字段名
* @param desc 字段类型
* @param signature 泛型描述
* @param value 默认值
* @return
*/
@Override
FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
return super.visitField(access, name, desc, signature, value)
}
-
visitMethod
/**
* 当扫描器扫描到类的方法时调用
*
* @param access 方法的修饰符
* @param name 方法名
* @param desc 方法签名
* @param signature 表示泛型相关的信息
* @param exceptions 表示将会抛出的异常,如果方法没有抛出异常,则参数为空
* @return
*/
@Override
MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
return super.visitMethod(access, name, desc, signature, exceptions)
}
方法的签名格式:
在ASM中不同的类型对应不同的代码,我们做JNI开发的时候,调用JNI的方法,传的参数类型也需要转成方法签名的格式,详细的对应关系如下表:
![](https://img.haomeiwen.com/i8669504/7f4a9000c9254c00.png)
示例:完成指定方法耗时监测
对该类使用javac命令生成.class文件,放入当前目录下:
public class InjectTest {
public void sayHello() {
long l = System.currentTimeMillis();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
long cost = System.currentTimeMillis() - l;
System.out.println("The cost time of " + cost + "ms");
}
}
在执行类中做test检测方法,将InjectTest.class进行字节码插桩操作,再输出新的字节码文件。
public void test() {
try {
File file = new File("src/main/java/com/example/asmbytecode/simpledemo/InjectTest.class");
FileInputStream fis = new FileInputStream(file);
//将class文件转成流
ClassReader cr = new ClassReader(fis);
//ClassWriter.COMPUTE_FRAMES 参数意义: 自动计算栈帧 和 局部变量表的大小
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
//执行分析
cr.accept(new MyClassVisitor(Opcodes.ASM5, cw), ClassWriter.COMPUTE_FRAMES);
System.out.println("Success!");
//执行了插桩之后的字节码数据输出
byte[] bytes = cw.toByteArray();
FileOutputStream fos = new FileOutputStream("src/main/java/com/example/asmbytecode/simpledemo/InjectTest2.class");
fos.write(bytes);
fos.close();
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
自定义Classvisitor:
static class MyClassVisitor extends ClassVisitor {
public MyClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
//类似于动态代理的机制,会将执行的方法进行回调,然后在方法执行之前和之后做操作
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
return new MyMethodVisitor(api, methodVisitor, access, name, descriptor);
}
}
自定义AdviceAdapter处理访问到的属性,包括注解,方法执行的开始和结束等。
static class MyMethodVisitor extends AdviceAdapter {
private int startTimeId = -1;
/**
* 用变量区分方法是否需要执行插桩
*/
boolean inject = false;
private String methodName = null;
protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
methodName = name;
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
//descriptor为方法的注解类型 行如: Lcom/example/bytecodeProject/ASMTest
//如果方法的注解为ASMTest,则执行插桩代码
if (descriptor.equals("Lcom/example/asmbytecode/simpledemo/ASMTest")) {
inject = true;
}
return super.visitAnnotation(descriptor, visible);
}
@Override
protected void onMethodEnter() { //代码插入到方法头部
super.onMethodEnter();
if (!inject) {
return;
}
//在Java kotlin中写代码直接写,但是ASM写代码有最大区别,就是需要用方法签名的格式来写。
//long l = System.currentTimeMillis();
//要写如上一行代码的字节码,需要执行一个静态方法,类是System,方法名是currentTimeMillis,所以有如下代码:
startTimeId = newLocal(Type.LONG_TYPE);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitIntInsn(LSTORE, startTimeId);
}
@Override
protected void onMethodExit(int opcode) { //代码插入到方法结尾
super.onMethodExit(opcode);
if (!inject) {
return;
}
int durationId = newLocal(Type.LONG_TYPE);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(LLOAD, startTimeId);
mv.visitInsn(LSUB);
mv.visitVarInsn(LSTORE, durationId);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
mv.visitLdcInsn("The cost time of " + methodName + "() is ");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitVarInsn(LLOAD, durationId);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
mv.visitLdcInsn(" ms");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
}
指定了使用了ASMTest注解的方法,才会进行插桩。
留下问题:
如何在Android中找到所有class文件,而不是自己生成读取固定的文件?
参考:
https://www.jianshu.com/p/abba54baf617
网友评论