Android ASM字节码插桩

1.ASM

ASM是一个字节码操作框架,可用来动态生成字节码或者对现有的类进行增强。ASM可以直接生成二进制的class字节码,也可以在class被加载进虚拟机前动态改变其行为,比如方法执行前后插入代码、添加成员变量、修改父类、添加接口等等。

插桩就是将一段代码插入或者替换原本的代码。字节码插桩就是在编写的java源码编译成class字节码后,在Android下生成dex之前修改class文件,修改或者增强原有代码逻辑的操作。

编写好的代码经过编译后的class文件如下:

67649b728c5a411b94b084c231547789.png

 然后经过字节码插桩后如下:

349110f71c7f448c94ff04febc42766d.png

比如需要查看方法执行耗时,如果每一个方法都要自己手动加入这些内容,当不需要时也要一个个删去相应的代码。一个、两个方法还好,如果有10个、20个得多麻烦!所以可以利用注解来标记需要插桩的方法,结合编译后操作字节码来自动插入,当不需要时关掉插桩即可。这种AOP思想使开发者只需要关注插桩代码本身。

ASM框架就是操作java字节码的框架之一,按照class文件的格式,解析、修改、生成class,可以动态生成类或者增强现有类的功能。热修复、systrace都使用了字节码插桩。

这跟gson很像,因为JSON格式数据是基于文本的,我们只需要知道它的规则就能够轻松的生成、修改JSON数据。同样的Class字节码也有其自己的规则(格式)。操作JSON可以借助GSON来非常方便的生成、修改JSON数据。而字节码Class,同样可以借助Javassist/ASM来实现对其修改。

 

2.ASM的使用

①引入ASM依赖:

6669b21ba51d4879865095de3abe2be1.png

 使用testImplementation引入,表示只能在Java的单元测试中使用这个框架,对Android中的依赖关系没有任何影响。

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

②准备待插桩Class

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

InjectTest.java:

public class InjectTest {

    public static void main(String[] args) throws InterruptedException{

        Thread.sleep(1000);

    }

}

由于我们操作的是字节码插桩,也就是class文件,所以需要进入 test/java下面使用 javac对这个java类进行编译生成对应的class文件,具体操作是:在Android studio底部Terminal窗口,通过cd进入到test/java目录下,然后执行以下命令:

javac com/demo/test/InjectTest.java

执行上面的命令编译后,就会在test/java下面生成对应的InjectTest.class文件,这个class文件就是待插桩的文件。

③执行插桩

待插桩的class文件准备好了,接下来写个单元测试来执行插桩吧。利用ASM向main方法中插入一开始图中的记录函数执行时间的日志输出。

在test/java下新建ASMUnitTest.java文件:

public class ASMUnitTest {

    @Test

    public void test() throws IOException {

        //1 准备待分析的class

        FileInputStream fis = new FileInputStream("src/test/java/com/demo/test/InjectTest.class");

        //2 执行分析与插桩

        ClassReader cr = new ClassReader(fis); // ClassReader是class字节码的读取与分析引擎

        ClassWriter cw = new ClassWriter( ClassWriter.COMPUTE_FRAMES); // 写出器, COMPUTE_FRAMES表示自动计算栈帧和局部变量表的大小

        cr.accept(new MyClassVisitor(cw), ClassReader.EXPAND_FRAMES);  //执行分析,处理结果写入cw, EXPAND_FRAMES表示栈图以扩展格式进行访问

        //3、获得执行了插桩之后的字节码数据

        byte[] newClassBytes = cw.toByteArray();

        FileOutputStream fos = new FileOutputStream("src/test/java/com/demo/test/InjectTest1.class");

        fos.write(newClassBytes);

        fos.close();

    }

}

首先获取上一步生成的class,然后由ASM执行完插桩之后,将结果输出到 test/java目录下的InjectTest1.class文件。其中关键点就在于第2步中,即如何进行插桩:

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

public class MyClassVisitor extends ClassVisitor {

    public MyClassVisitor(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 MyMethodVisitor(api,mv, access, name, desc);

    }

}

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

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

//AdviceAdapter: 子类,对MethodVisitor进行了扩展, 能更加轻松的进行方法分析

public class MyMethodVisitor extends AdviceAdapter {

    protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {

        super(api, methodVisitor, access, name, descriptor);

    }

    private int start;

    @override

    protected void onMethodEnter() {

        super.onMethodEnter();

        //进入方法时,插入 long l = System.currentTimeMillis();   

        invokeStatic(Type.getType( "Ljava/lang/System;"), new Method( "currentTimeMillis", "()J")); //执行System.currentTimeMillis();

        start = newLocal(Type.LONG_TYPE); //创建本地LONG类型变量 

        storeLocal(start); //将上一步方法执行结果保存到创建的本地变量中

    }

    @override

     protected void onMethodExit(int opcode) {

        super.onMethodExit(opcode);

        //退出方法时,插入 long e = System.currentTimeMillis();  

        invokeStatic(Type.getType( "Ljava/lang/System;"), new Method( "currentTimeMillis", "()J"));

        int end = newLocal(Type.LONG_TYPE);

        storeLocal(end);

        //退出方法时,插入System.out.println( "execute" + (e - l) + "ms.");

        getStatic(Type.getType( "Ljava/lang/System;"),"out",Type.getType("Ljava/io" +"/PrintStream;")); //执行System.out

        newInstance(Type.getType( "Ljava/lang/StringBuilder;")); // 执行new StringBuilder分配内存

        dup(); //dup压入栈顶,让下面的INVOKESPECIAL 知道执行谁的构造方法创建StringBuilder

        invokeConstructor(Type.getType( "Ljava/lang/StringBuilder;"),new Method("<init>","()V")); //调用StringBuilder的构造方法

        visitLdcInsn("execute:"); 

        invokeVirtual(Type.getType( "Ljava/lang/StringBuilder;"),new Method("append","(Ljava/lang/String;) Ljava/lang/StringBuilder;")); // 调用StringBuilder的append方法    

        loadLocal(end); // 加载方法结束的时间

        loadLocal(start); //加载方法开始的时间

        math(SUB,Type.LONG_TYPE); //减法

        invokeVirtual(Type.getType( "Ljava/lang/StringBuilder;"),new Method("append","(J)Ljava/lang/StringBuilder;")); // 调用St

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值