快捷搜索:  汽车  科技

cto 算法:ASM字节码插桩 QQ空间的热修复解决方案核心技术

cto 算法:ASM字节码插桩 QQ空间的热修复解决方案核心技术/** * 1、准备待分析的class */ FileInputStream fis = new FileInputStream ("xxxxx/test/java/InjectTest.class"); /** * 2、执行分析与插桩 */ //class字节码的读取与分析引擎 ClassReader cr = new ClassReader(fis); // 写出器 COMPUTE_FRAMES 自动计算所有的内容,后续操作更简单 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); //分析,处理结果写入cw EXPAND_FRAMES:栈图以扩展格式进行访问 cr.accept(new ClassAdapterVisitor(cw) ClassReader.EXPAND_FRAMES);

因此,我们可以在AS中加入:

cto 算法:ASM字节码插桩 QQ空间的热修复解决方案核心技术(1)

同时,需要注意的是:我们使用 testImplementation引入,这表示我们只能在Java的单元测试中使用这个框架,对我们Android中的依赖关系没有任何影响。

AS中使用gradle的Android工程会自动创建Java单元测试与Android单元测试。测试代码分别在test与androidTest。

3.2、准备待插桩Class

在 test/java下面创建一个Java类:

public class InjectTest { public static void main(String[] args) { } }

由于我们操作的是字节码插桩,所以可以进入 test/java下面使用 javac对这个类进行编译生成对应的class文件。

javac InjectTest.java

3.3、执行插桩

因为 main方法中没有任何输出代码,我们输入命令:javaInjectTest执行这个Class不会有任何输出。那么我们接下来利用 ASM,向 main方法中插入一开始图中的记录函数执行时间的日志输出。

在单元测试中写入测试方法

/** * 1、准备待分析的class */ FileInputStream fis = new FileInputStream ("xxxxx/test/java/InjectTest.class"); /** * 2、执行分析与插桩 */ //class字节码的读取与分析引擎 ClassReader cr = new ClassReader(fis); // 写出器 COMPUTE_FRAMES 自动计算所有的内容,后续操作更简单 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES); //分析,处理结果写入cw EXPAND_FRAMES:栈图以扩展格式进行访问 cr.accept(new ClassAdapterVisitor(cw) ClassReader.EXPAND_FRAMES); /** * 3、获得结果并输出 */ byte[] newClassBytes = cw.toByteArray(); File file = new File("xxx/test/java2/"); file.mkdirs(); FileOutputStream fos = new FileOutputStream ("xxx/test/java2/InjectTest.class"); fos.write(newClassBytes); fos.close();

关于ASM框架本身的设计,我们这里先不讨论。上面的代码会获取上一步生成的class,然后由ASM执行完插桩之后,将结果输出到 test/java2目录下。其中关键点就在于第2步中,如何进行插桩。

把class数据交给 ClassReader,然后进行分析,类似于XML解析,分析结果会以事件驱动的形式告知给accept的第一个参数 ClassAdapterVisitor。

public class ClassAdapterVisitor extends ClassVisitor { public ClassAdapterVisitor(ClassVisitor cv) { super(Opcodes.ASM7 cv); } @Override public MethodVisitor visitMethod(int access String name String desc String signature String[] exceptions) { System.out.println("方法:" name " 签名:" desc); MethodVisitor mv = super.visitMethod(access name desc signature exceptions); return new MethodAdapterVisitor(api mv access name desc); } }

分析结果通过 ClassAdapterVisitor获得,一个类中会存在方法、注解、属性等,因此 ClassReader会将调用 ClassAdapterVisitor中对应的 visitMethod、 visitAnnotation、 visitField这些 visitXX方法。

我们的目的是进行函数插桩,因此重写 visitMethod方法,在这个方法中我们返回一个 MethodVisitor方法分析器对象。一个方法的参数、注解以及方法体需要在 MethodVisitor中进行分析与处理。

package com.enjoy.asminject.example; import com.enjoy.asminject.ASMTest; import org.objectweb.asm.AnnotationVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Type; import org.objectweb.asm.commons.AdviceAdapter; import org.objectweb.asm.commons.Method; /** * AdviceAdapter: 子类 * 对methodVisitor进行了扩展, 能让我们更加轻松的进行方法分析 */ public class MethodAdapterVisitor extends AdviceAdapter { private Boolean inject; protected MethodAdapterVisitor(int api MethodVisitor methodVisitor int access String name String descriptor) { super(api methodVisitor access name descriptor); } /** * 分析方法上面的注解 * 在这里干嘛??? * <p> * 判断当前这个方法是不是使用了injecttime,如果使用了,我们就需要对这个方法插桩 * 没使用,就不管了。 * * @param desc * @param visible * @return */ @Override public AnnotationVisitor visitAnnotation(String desc Boolean visible) { if (Type.getDescriptor(ASMTest.class).equals(desc)) { System.out.println(desc); inject = true; } return super.visitAnnotation(desc visible); } private int start; @Override protected void onMethodEnter() { super.onMethodEnter(); if (inject) { //执行完了怎么办?记录到本地变量中 invokeStatic(Type.getType("Ljava/lang/System;") new Method("currentTimeMillis" "()J")); start = newLocal(Type.LONG_TYPE); //创建本地 LONG类型变量 //记录 方法执行结果给创建的本地变量 storeLocal(start); } } @Override protected void onMethodExit(int opcode) { super.onMethodExit(opcode); if (inject){ invokeStatic(Type.getType("Ljava/lang/System;") new Method("currentTimeMillis" "()J")); int end = newLocal(Type.LONG_TYPE); storeLocal(end); getStatic(Type.getType("Ljava/lang/System;") "out" Type.getType("Ljava/io" "/PrintStream;")); //分配内存 并dup压入栈顶让下面的INVOKESPECIAL 知道执行谁的构造方法创建StringBuilder newInstance(Type.getType("Ljava/lang/StringBuilder;")); dup(); invokeConstructor(Type.getType("Ljava/lang/StringBuilder;") new Method("<init>" "()V")); visitLdcInsn("execute:"); invokeVirtual(Type.getType("Ljava/lang/StringBuilder;") new Method("append" "(Ljava/lang/String;)Ljava/lang/StringBuilder;")); //减法 loadLocal(end); loadLocal(start); math(SUB Type.LONG_TYPE); invokeVirtual(Type.getType("Ljava/lang/StringBuilder;") new Method("append" "(J)Ljava/lang/StringBuilder;")); invokeVirtual(Type.getType("Ljava/lang/StringBuilder;") new Method("toString" "()Ljava/lang/String;")); invokeVirtual(Type.getType("Ljava/io/PrintStream;") new Method("println" "(Ljava/lang/String;)V")); } } }

MethodAdapterVisitor继承自 AdviceAdapter,其实就是 MethodVisitor 的子类, AdviceAdapter封装了指令插入方法,更为直观与简单。

上述代码中 onMethodEnter进入一个方法时候回调,因此在这个方法中插入指令就是在整个方法最开始加入一些代码。我们需要在这个方法中插入 longs=System.currentTimeMillis();。在 onMethodExit中即方法最后插入输出代码。

@Override protected void onMethodEnter() { super.onMethodEnter(); if (inject) { //执行完了怎么办?记录到本地变量中 invokeStatic(Type.getType("Ljava/lang/System;") new Method("currentTimeMillis" "()J")); start = newLocal(Type.LONG_TYPE); //创建本地 LONG类型变量 //记录 方法执行结果给创建的本地变量 storeLocal(start); } }

这里面的代码怎么写?其实就是 longs=System.currentTimeMillis();这句代码的相对的指令。我们可以先写一份代码

void test(){ //插入的代码 long s = System.currentTimeMillis(); /** * 方法实现代码.... */ //插入的代码 long e = System.currentTimeMillis(); System.out.println("execute:" (e-s) " ms."); }

然后使用 javac编译成Class再使用 javap-c查看字节码指令。也可以借助插件来查看,就不需要我们手动执行各种命令。

cto 算法:ASM字节码插桩 QQ空间的热修复解决方案核心技术(2)

安装完成之后,可以在需要插桩的类源码中点击右键:

cto 算法:ASM字节码插桩 QQ空间的热修复解决方案核心技术(3)

点击 ASM Bytecode Viewer 之后会弹出

cto 算法:ASM字节码插桩 QQ空间的热修复解决方案核心技术(4)

所以第20行代码: longs=System.currentTimeMillis();会包含两个指令: INVOKESTATIC与 LSTORE。

再回到 onMethodEnter方法中

@Override protected void onMethodEnter() { super.onMethodEnter(); if (inject) { //invokeStatic指令,调用静态方法 invokeStatic(Type.getType("Ljava/lang/System;") new Method("currentTimeMillis" "()J")); //创建本地 LONG类型变量 start = newLocal(Type.LONG_TYPE); //store指令 将方法执行结果从操作数栈存储到局部变量 storeLocal(start); } }

而 onMethodExit也同样根据指令去编写代码即可。最终执行完插桩之后,我们就可以获得修改后的class数据。

四、Android中的实现

在Android中实现,我们需要考虑的第一个问题是 如何获得所有的Class文件来判断是否需要插桩 。Transform就是干这件事情的。

猜您喜欢: