ASM 是什么
- ASM 是字节码操作库,支持对字节码进行编辑,实现类、属性和方法的增删改查;
- 字节码操作库还有 Javaassit 库可以选择,但 ASM 灵活度和效率都是最高的;
- ASM 可以直接产生二进制的 class 文件,也可以增强既有类的功能。
- Java class 被存储在严格格式定义的 .class文件里,这些类文件拥有足够的元数据来解析类中的所有元素,包括:
- 类名称
- 方法
- 属性
- Java 字节码(指令)。
- Java class 被存储在严格格式定义的 .class文件里,这些类文件拥有足够的元数据来解析类中的所有元素,包括:
ASM 字节码插桩
-
利用操作 ASM 字节码实现方法计时,可以的做法是修改 class 文件,在目标方法开始和结束时插入本来需要手动埋点的代码,这个过程称之为字节码插桩。
-
Gradle Transform
- Gradle Transform 是 Android 官方提供给开发者在项目构建阶段即由 class 到 dex 转换期间修改 class 文件的一套 api。
- 目前比较经典的应用是字节码插桩、代码注入技术。
- Gradle Transform 是 Android 官方提供给开发者在项目构建阶段即由 class 到 dex 转换期间修改 class 文件的一套 api。
- 上图所示是 Android 打包流程:.java 文件 -> .class 文件 -> .dex 文件;
- 只要在红圈处拦截住,拿到所有方法进行修改完再放生就可以了,而做到这一步也不难,Google 官方在Android Gradle 的 1.5.0 版本以后提供了 Transfrom API,允许第三方 Plugin 在打包 dex 文件之前的编译过程中操作 .class 文件;
- ASM 字节码插桩,要做的就是实现 Transform 进行 .class 文件遍历拿到所有方法,修改完成对原文件进行替换。
ASM 框架中的核心类
- ClassReader
- 该类用来解析编译过的class字节码文件。
- ClassWriter
- 该类用来重新构建编译后的类,比如说修改类名、属性以及方法,甚至可以生成新的类的字节码文件。
- ClassViitor
- 主要负责 “拜访” 类成员信息,包括:
- 标记在类上的注解,
- 类的构造方法,
- 类的字段,
- 类的方法,
- 静态代码块。
- 主要负责 “拜访” 类成员信息,包括:
- AdviceAdapter
- 实现了 MethodVisitor 接口,主要负责 “拜访” 方法的信息,用来进行具体的方法字节码操作。
ASM 插桩实现方案
- 方案简介
- class 文件是按照 JVM 规范格式存储的二进制文件,本质上是一个表,记录了类的常量池、访问标志、属性和方法等。
- ASM 库不仅能够对 class 文件进行解读,还提供了方便的 API 进行字节码的修改,支持直接产生二进制 class 文件。
- ASM 提供了基于事件的 API:
- ClassReader 用于读取 class 文件的二进制流;
- ClassVisitor 以事件的形式输出 class 的结构信息;
- ClassWriter 则用于把修改后的字节码生成二进制流。
-
示例:监听点击事件
-
步骤一:通过 visitMethod 拿到方法名,判断这个方法是不是要监控的方法
-
ASM 读取分析 class 文件
- class 文件在工程的
app/build/intermediates/classes
目录下public static void main(String[] args) { try { File classFile = new File("./source/MainActivity.class"); File dir = new File("."); transformClassFile(dir, classFile) } catch (Exception e){} } private static File transformClassFile(File dir, File sourceFile){ String className = sourceFile.getName(); // 得到class文件二进制流 FileInputStream fileInputStream = new FileInputStream(sourceFile); byte[] sourceClassBytes = IOUtils.toByteArray(fileInputStream); // 定义classWriter,用于输出修改后的二进制流 ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); // 自定义ClassVisitor, 负责字节码的消费 MyClassVisitor myClassVisitor = new MyClassVisitor(classWriter); // ClassReader负责字节码的读取 ClassReader classReader = new ClassReader(sourceClassBytes); // 开始字节码处理 classReader.accept(myClassVisitor, 0); // 生成二进制流并保存成新的文件 byte[] destByte = classWriter.toByteArray(); File modified = new File(dir, className) if (modified.exists()) { modified.delete() } modified.createNewFile(); new FileOutputStream(modified).write(destByte) return modified; } private static class MyClassVisitor extends ClassVisitor { public MyClassVisitor(ClassVisitor classVisitor) { super(Opcodes.ASM6, classVisitor); } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { System.out.println("visit: access: " + access + " ,name: " + name + " , superName: " + superName + " ,singature: " + signature + ", interfaces: " + interfaces.join("/")); super.visit(version, access, name, signature, superName, interfaces); } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { System.out.println("visitMethod: access: " + access + " ,name: " + name + " , desc: " + descriptor + " ,singature: " + signature); MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); MethodVisitor myMv = new MethodVisitor(Opcodes.ASM6, mv) { @Override AnnotationVisitor visitAnnotation(String desc, boolean visible) { System.out.println("visitAnnotation: desc: " + desc); return super.visitAnnotation(desc, visible) } @Override void visitCode() { super.visitCode() } } return myMv; } }
- class 文件在工程的
- 用 ClassReader 读取 class 文件,并用自定义的 ClassVisitor 接收事件,查看输出:
visit: access: 33 ,name: com/example/wangkai/MainActivity , superName: android/support/v7/app/AppCompatActivity ,singature: null, interfaces: visitMethod: access: 1 ,name: <init> , desc: ()V ,singature: null visitMethod: access: 4 ,name: onCreate , desc: (Landroid/os/Bundle;)V ,singature: null visitMethod: access: 1 ,name: fun , desc: ()V ,singature: null visitAnnotation: desc: Lcom/example/wangkai/annotation/TraceTime;
- 这里通过 visit 回调可以读取到 class 的名字、父类名和接口,这样就可以判断出一个类是否是我们要插桩的白名单页面,是不是 Activity 子类以及是否实现了点击事件接口 View$onClickListener(实现对点击事件的监控)
- 通过在 visitMethod 方法里返回自定义的 MethodVisitor 对象,拿到方法上的注解,从而可以知道这个方法是否是要插桩的方法
- 上面的输出中,visitCode 表示方法开始执行,如果能在这里插入代码,那代码就能在原始代码执行前执行。
- 我们已经找到了切入点,下一步就是插入代码了。
-
-
步骤二:插入埋点代码
- 插入代码要难一些,因为我们是在字节码层面操作,插入的也只能是字节码,这就需要对字节码有一定了解。
- 包括局部变量表和操作数栈的概念,常见指令(ALOAD, INVOKEVIRTUAL等)的含义。
-
埋点时,我们需要插入这样的代码:
private static class MyClickListener implements View.OnClickListener{ @Override public void onClick(View v) { ClickAgent.click(v); //待插入代码,方法里获取view的ID和当前时间,实现对点击事件的记录 Log.d(TAG, "onClick: "); } }
-
上面这步操作的是,通过 ASM methodVisitor 提供的 API,把 ClickAgent.click(v) 的字节码,注入到原始 onClick 方法里。
- 查看字节码:
L0 LINENUMBER 27 L0 ALOAD 1 INVOKESTATIC com/example/wangkai/ClickAgent.click (Landroid/view/View;)V L1 LINENUMBER 28 L1 LDC "MainActivity" LDC "onClick: "
- INVOKESTATIC android/util/Log.d (Ljava/lang/String;Ljava/lang/String;)I
可以看到ClickAgent.click(v)对应的字节码是两行; - ALOAD 1 表示把局部变量表里索引为 1 的值,推到操作数栈上,也就是参数值 View v。
- 对应到 ASM,是 methodVisitor.visitVarInsn(Opcodes.ALOAD, 1);
- INVOKESTATIC com/example/wangkai/ClickAgent.click (Landroid/view/View;)V就是调用 ClickAgent 的静态方法 click。
- 对应到 ASM,是 methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, “com/example/wangkai/ClickAgent”, “click”, “(Landroid/view/View;)V”, false)
- INVOKESTATIC android/util/Log.d (Ljava/lang/String;Ljava/lang/String;)I
- 当我们在 visitorMethod 回调里判断 name、desc 和 signature 和原始方法一致,并且该类实现的 interfaces 包含了 View$onClickListener 时,就可以注入了。
- 具体的注入代码如下:
@Override void visitCode() { super.visitCode() mv.visitVarInsn(Opcodes.ALOAD, 1); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/example/wangkai/ClickAgent", "click", "(Landroid/view/View;)V", false) }
- 修改后执行,会生成插桩后的 class 文件,可以用 JD_GUI 查看插桩后的效果。
- 实际编码中,我们可以借助于 Bydecode Outline 插件。
- 以上简单介绍了无埋点插桩实现的过程。
- 实际的插件工程要复杂,需要考虑:
- 黑白名单处理;
- Manifest文件读取;
- 插桩的统一处理等。
- 另外,考虑到实现的复杂度和对性能的消耗,无埋点并不能完全代替手工埋点;
- 部分埋点信息仍然需要手工补全。
- 实际的插件工程要复杂,需要考虑: