看完这篇你还不会ASM字节码插桩,我吃x!

ASM字节码插桩

1、什么是插桩

QQ空间曾经发布的热修复解决方案中利用Javaassist库实现向类的构造函数中插入一段代码解决CLASS_ISPREVERIFIED 问题。包括了Instant Run的实现以及参照Instant Run实现的热修复美团Robus等都利用到了插桩技术。html

插桩就是将一段代码插入或者替换本来的代码。字节码插桩顾名思义就是在咱们编写的源码编译成字节码(Class)后,在Android下生成dex以前修改Class文件,修改或者加强原有代码逻辑的操做。java

插桩前

插桩后

咱们须要查看方法执行耗时,若是每个方法都须要本身手动去加入这些内容,当不须要时也须要一个个删去相应的代码。1个、两个方法还好,若是有10个、20个得多麻烦!因此能够利用注解来标记须要插桩的方法,结合编译后操做字节码来帮助咱们自动插入,当不须要时关掉插桩便可。这种AOP思想让咱们只须要关注插桩代码自己。android

2、字节码操做框架

上面咱们提到QQ空间使用了Javaassist来进行字节码插桩,除了Javaassist以外还有一个应用更为普遍的ASM框架一样也是字节码操做框架,Instant Run包括AspectJ就是借助ASM来实现各自的功能。web

咱们很是熟悉的JSON格式数据是基于文本的,咱们只须要知道它的规则就可以轻松的生成、修改JSON数据。一样的Class字节码也有其本身的规则(格式)。操做JSON能够借助GSON来很是方便的生成、修改JSON数据。而字节码Class,一样能够借助Javassist/ASM来实现对其修改。shell

打包流程

字节码操做框架的做用在于生成或者修改Class文件,所以在Android中字节码框架自己是不须要打包进入APK的,只有其生成/修改以后的Class才须要打包进入APK中。它的工做时机在上图Android打包流程中的生成Class以后,打包dex以前。api

3、ASM的使用

因为ASM具备相对于Javassist更好的性能以及更高的灵活行,咱们这篇文章以使用ASM为主。在真正利用到Android中以前,咱们能够先在Java程序中完成对字节码的修改测试。markdown

3.一、在AS中引入ASM

ASM能够直接从jcenter()仓库中引入,因此咱们能够进入:bintray.com/进行搜索app

jcenter搜索

点击图中标注的工件进入,能够看到最新的正式版本为:7.1。框架

asm版本查看

所以,咱们能够在AS中加入:ide

引入asm

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

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

3.二、准备待插桩Class

test/java下面建立一个Java类:

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

复制代码

因为咱们操做的是字节码插桩,因此能够进入test/java下面使用javac对这个类进行编译生成对应的class文件。

javac InjectTest.java
复制代码

3.三、执行插桩

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

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

/** * 一、准备待分析的class */
        FileInputStream fis = new FileInputStream
                ("xxxxx/test/java/InjectTest.class");

        /** * 二、执行分析与插桩 */
        //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);


        /** * 三、得到结果并输出 */
        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中对应的visitMethodvisitAnnotationvisitField这些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进入一个方法时候回调,所以在这个方法中插入指令就是在整个方法最开始加入一些代码。咱们须要在这个方法中插入long s = 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);
        }
    }
复制代码

这里面的代码怎么写?其实就是long s = System.currentTimeMillis();这句代码的相对的指令。咱们能够先写一份代码

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

而后使用javac编译成Class再使用javap -c查看字节码指令。也能够借助插件来查看,就不须要咱们手动执行各类命令。 插件安装

安装完成以后,能够在须要插桩的类源码中点击右键:

查看字节码

点击ASM Bytecode Viewer以后会弹出

字节码

因此第20行代码:long s = System.currentTimeMillis();会包含两个指令:INVOKESTATICLSTORE

再回到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数据。

4、Android中的实现

在Android中实现,咱们须要考虑的第一个问题是如何得到全部的Class文件来判断是否须要插桩。Transform就是干这件事情的。

相关文章
相关标签/搜索