Android ASM字节码插桩

本文介绍了ASM字节码操作框架在Android中的应用,用于动态生成或修改字节码,实现方法执行前后插入代码。通过示例详细讲解了如何使用ASM进行字节码插桩,包括引入依赖、准备待插桩Class、执行插桩过程以及如何创建字节码注入时间方法器。同时,展示了如何在Android项目中测试插桩效果,证明了插桩的成功。

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;")); // 调用StringBuilder的append方法    

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

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

    }

    @override

    public AnnotationVisitor visitAnnotation(String description, boolean visible) { //获取注解

        System.out.println("方法名为:" + getName() + "对应的注解为:" + description);

        return super.visitAnnotation(description, visible);

    }

}

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

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

onMethodEnter和onMethodExit方法里的代码怎么写?其实onMethodEnter方法里就是 long s = System.currentTimeMillis();这句代码的相对的指令。而onMethodExit方法里就是long e = System.currentTimeMillis();   System.out.println("execute" + (e - s) + "ms.");这两句代码相对应的指令。

我们可以先写一份代码:

void test(){

     //进入方法时插入的代码

     long s = System.currentTimeMillis();

    // 退出方法时插入的代码

    long e = System.currentTimeMillis(); 

    System.out.println("execute" + (e - s) + "ms.");

}

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

093831f1632a4e4eb1ba93b5df2b3b8e.jpg

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

d133b44836a14bc892836db7a15340c1.png

 点击ASM Bytecode Viewer之后会弹出

cebbad0afcd44ea3ad75f96ab46bc146.jpg

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

再回到 onMethodEnter方法中:

@override

protected void onMethodEnter() {

    super.onMethodEnter();

        //invokeStatic指令,调用静态方法

        invokeStatic(Type.getType( "Ljava/lang/System;"), new Method( "currentTimeMillis", "()J")); // 相当于java中System.currentTimeMillis();这一句代码

      //用一个本地变量接收上一步的执行结果

      int start = newLocal(Type.LONG_TYPE); //start表示当前long类型的本地变量的索引

     storeLocal(start);  //store指令,将方法执行结果从操作数栈存储到局部变量

}

invokeStatic指令涉及到几个名词:

①类型描述符

Java代码中的类型,在字节码中有相应的表示协议:

Java Type        Type description

   boolean                    Z

     char                        C

      byte                        B

     short                       S

      int                           I

     float                        F

      long                       J

     double                    D

    object           Ljava/lang/Object;

     int[]                         [I

  Object[][]          [[Ljava/lang/Object;

     void                        V

  引用类型                  L

(1)Java基本类型的描述符是单个字符,例如Z表示boolean、C表示char

(2)类的类型的描述符是这个类的全限定名,前面加上字符L , 后面跟上一个「;」,例如String的类型描述符为Ljava/lang/String;

(3)数组类型的描述符是一个方括号后面跟有该数组元素类型的描述符,多维数组则使用多个方括号。

借助上面的协议分析,想要看到字节码中参数的类型,就比较简单了。

②方法描述符

方法描述符(方法签名)是一个类型描述符列表,它用一个字符串描述一个方法的参数类型和返回类型。

方法描述符以左括号开头,然后是每个形参的类型描述符,然后是是右括号,接下来是返回类型的类型描述符,例如,该方法返回void,则是V,要注意的是,方法描述符中不包含方法的名字或参数名。

比如:

void m(int i, float f)对应的方法描述符是(IF)V ,表明该方法会接收一个int和float型参数,且无返回值。

int m(Object o)对应的方法描述符是(Ljava/lang/Object;)I 表示接收Object型参数,返回int。

int[] m(int i, String s)对应的方法描述符是(ILjava/lang/String;)[I 表示接受int和String,返回一个int[]。

Object m(int[] i)对应的方法描述符是 ([I)Ljava/lang/Object; 表示接受一个int[],返回Object。

 

同样,onMethodExit也根据指令去编写代码:

onMethodExit中需要插入的代码在ASMByteCode中的格式如下:

afb93642df6649419ac5d3c5c0ef5e88.jpg

 对应的代码如下:

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;")); //执行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;")); // 调用StringBuilder的append方法    

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

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

最终执行完插桩之后,就可以获得修改后的class数据。

 

3.有选择性的插桩

现在存在一个问题,就是待插桩的class里所有的方法都被加入了插桩的代码。

插桩后生成的的InjectTest.class如下:

public class InjectTest {

    public InjectTest() {

        long var1 = System.currentTimeMillis();

        long var3 = System.currentTimeMillis();

        System.out.println("execute:" + (var3 - var1) + "ms.";

    }

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

        long var1 = System.currentTimeMillis();

        Thread.sleep(1000L);

        long var3 = System.currentTimeMillis();

        System.out.println("execute:" + (var3 - var1) + "ms.";

    }

}

如果只想在main方法里插桩,而不想在构造方法里插桩,这时候可以使用注解。

①创建注解类

新建ASMTest类:

ASMTest.java:

@Retention(RetentionPolicy.CLASS)

@Target(ElementType.METHOD)

public @interface ASMTest{

}

②在需要插桩的方法上面添加注解

InjectTest.java:

public class InjectTest {

    @ASMTest

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

        Thread.sleep(1000L);

    }

    public void aa() { //新增一个方法,没有添加注解,因此不会执行插桩代码

    }

}

接下来修改MyMethodVisitor类,在MyMethodVisitor里重写visitAnnotation方法:

public class MyMethodVisitor extends AdviceAdapter {

   ……

    boolean inject = false;

    @override

    public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {

        if("Lcom/demo/test/ASMTest;".equals( descriptor) {

            inject = true;// 这个方法的注解为Lcom/demo/test/ASMTest;的时候,将inject置为true表示需要插桩

        }

        return super.visitAnnotation(descriptor, visible);

    }

    @override

    protected void onMethodEnter(){

        if(!inject) {

            return;

        }

        ……

    }

    @override

    protected void onMethodExit(int opcode){

        if(!inject) {

            return;

        }

        ……

    }

}

通过visitAnnotation方法可以判断每个方法的注解,然后在每个方法的onMethodEnter和onMethodExit里根据是否有这个注解来判断是否需要执行插桩。

修改后,插桩生成的的InjectTest.class如下:

public class InjectTest {

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

        long var1 = System.currentTimeMillis();

        Thread.sleep(1000L);

        long var3 = System.currentTimeMillis();

        System.out.println("execute:" + (var3 - var1) + "ms.";

    }

    public void aa() {

    }

}

这样,就实现了只在加了注解的main方法里插入了代码。

 

4.Android的实现

在Android中执行插桩,第一个问题就是如何获得所有编译好的class文件。

首先看一下Android工程的构建过程:

10e89025cfb1483ea20cc779abefe636.jpg

 ①Android Resources–>通过aapt–>R.java

②aidl Files–>通过aidl–>java interface

③(R.java、Android Resouce code、java interface)–>java compile–>.class Files

④(.class Files、3rd Party Libraries and class Files)–>dex 编译器–>.dex Files

⑤(dex Files、Other Resources)–>Apk Builder–>Android Package(.apk)–>jar signer–>Signed Apk

字节码操作框架的作用在于生成或者修改class文件,因此在Android中字节码框架本身是不需要打包进入APK的,只有其生成/修改之后的class才需要打包进入APK中。它的工作时机在上图Android打包流程中的生成Class之后,打包dex之前。

要获得所有编译好的class文件,这里需要用到Transform。

Transform是Android 官方插件提供给开发者在项目构建阶段由class到dex转换之前修改class文件的一套API。目前典型的应用场景就是字节码插桩。通过Transform可以得到所有的class字节码,自定义的Transfrom会先执行,执行的结果做为参数进行传递。

 

①新建一个插件类

public class APMPlugin implements Plugin<Project> {

    @Override

    public void apply(Project project) {

        BaseExtension android = project.getExtensions().getByType(BaseExtension.class);

        //注册一个Transform

        android.registerTransform(new ASMTransfrom());

    }

}

android 插件能够获得所有的class,并通过接口的形式暴露出来。

②创建一个ASM

public class ASMTransform extends Transform {

    @Override

    public String getName() {

        return "ms_asm";

    }

    // 处理所有class

    @Override

    public Set<QualifiedContent.ContentType> getInputTypes() {

        return TransformManager.CONTENT_CLASS;

    }

    //范围仅仅是主项目所有的类

    @Override

    public Set<? super QualifiedContent.Scope> getScopes() {

        return TransformManager.PROJECT_ONLY;

    }

    //不使用增量

    @Override

    public boolean isIncremental() {

        return false;

    }

    //android插件将所有的class通过这个方法告诉给我们

    @Override

    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {

        super.transform(transformInvocation);

        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();

        //清理上次缓存的文件信息

        outputProvider.deleteAll();

        //得到所有的输入

        Collection<TransformInput> inputs = transformInvocation.getInputs();

        for (TransformInput input : inputs) {

            // 处理class目录

            for (DirectoryInput directoryInput : input.getDirectoryInputs()) {

                // 直接复制输出到对应的目录

                String dirName = directoryInput.getName();

                File src = directoryInput.getFile();

                System.out.println("输出class文件:" + src);

                String md5Name = DigestUtils.md5Hex(src.getAbsolutePath());

                //得到输出class文件的目录

                File dest = outputProvider.getContentLocation(dirName + md5Name,

                        directoryInput.getContentTypes(), directoryInput.getScopes(),

                        Format.DIRECTORY);

                //执行插桩操作

                processInject(src, dest);

            }

            // 处理jar(依赖)的class

            for (JarInput jarInput : input.getJarInputs()) {

                String jarName = jarInput.getName();

                File src = jarInput.getFile();

                System.out.println("输出jar包:" + src);

                String md5Name = DigestUtils.md5Hex(src.getAbsolutePath());

                if (jarName.endsWith(".jar")) {

                    jarName = jarName.substring(0, jarName.length() - 4);

                }

                File dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR);

                FileUtils.copyFile(src, dest);

            }

        }

    }

    private void processInject(File src, File dest) throws IOException {

        String dir = src.getAbsolutePath();

        FluentIterable<File> allFiles = FileUtils.getAllFiles(src);

        for (File file : allFiles) {

            //得到文件输入流

            FileInputStream fis = new FileInputStream(file);

            //得到字节码Reader

            ClassReader cr = new ClassReader(fis);

            //得到写出器

            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);

            //将注入的时间信息,写入

            cr.accept(new ClassInjectTimeVisitor( cw,file.getName()), ClassReader.EXPAND_FRAMES);

            byte[] newClassBytes = cw.toByteArray();

            String absolutePath = file.getAbsolutePath();

            String fullClassPath = absolutePath.replace(dir, "");

            //将得到的字节码信息 写如输出目录

            File outFile = new File(dest, fullClassPath);

            FileUtils.mkdirs(outFile.getParentFile());

            FileOutputStream fos = new FileOutputStream(outFile);

            fos.write(newClassBytes);

            fos.close();

        }

    }

}

继承Transform 重写父类的方法(com.android.build.api.transform.Transform)是这个别弄错了。

getName() 返回transfrom的方法名,这个随便定义。

getInputTypes() 得到需要处理的内容类型,TransformManager.CONTENT_CLASS 这个表示字节码。

getScopes() 返回的是处理范围,比如是整个项目还是仅仅主app等。

isIncremental() 是否增量。

transform() 这个方法会回调我们需要的所有类信息。

③创建字节码注入时间方法器

public class ClassInjectTimeVisitor extends ClassVisitor {

    //得到类名

    private String mClassName;

    public ClassInjectTimeVisitor(ClassVisitor cv, String fileName) {

        super(Opcodes.ASM5, cv);

        mClassName = fileName.substring(0, fileName.lastIndexOf("."));

    }

    /**

     * 访问方法

     * @param access 方法的访问flag

     * @param name 方法名

     * @param desc 描述信息

     * @param signature 签名信息

     * @param exceptions

     * @return

     */

    @Override

    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {

        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);

        return new MethodAdapterVisitor(mv, access, name, desc, mClassName);

    }

}

visitMethod这个方法负责拦截所有的方法,并初始化一个方法适配器MethodAdapterVisitor。

MethodAdapterVisitor,负责具体的插桩代码逻辑。

④准备需要插入的class信息

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

    long start = System.currentTimeMillis();

    Thread.sleep(1000);

    long end = System.currentTimeMillis();

    System.out.println("execute:"+(end-start)+" ms.");

}

插入方法耗时统计:

需要在方法开始插入这行代码:long start = System.currentTimeMillis();

结尾处插入:

​ long end = System.currentTimeMillis();

​ System.out.println(“execute:”+(end-start)+" ms.");

将上面的java信息转成ASM Bytecode,以便方法的进行插桩注入。

{

    methodVisitor = classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "main", "([Ljava/lang/String;)V", null, new String[]{"java/lang/InterruptedException"});

    methodVisitor.visitCode();

   //long start = System.currentTimeMillis();

    Label label0 = new Label();

    methodVisitor.visitLabel(label0);

    methodVisitor.visitLineNumber(7, label0);

    methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);

    methodVisitor.visitVarInsn(LSTORE, 1);

 

    // 下面是执行的 Thread.sleep(1_000); 

    Label label1 = new Label();

    methodVisitor.visitLabel(label1);

    methodVisitor.visitLineNumber(9, label1);

    methodVisitor.visitLdcInsn(new Long(1000L));

    methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "sleep", "(J)V", false);

 

    //下面是执行的 long end = System.currentTimeMillis();和System.out.println("execute:"+(end-start)+" ms.");

    Label label2 = new Label();

    methodVisitor.visitLabel(label2);

    methodVisitor.visitLineNumber(11, label2);

    methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);

    methodVisitor.visitVarInsn(LSTORE, 3);

    Label label3 = new Label();

    methodVisitor.visitLabel(label3);

    methodVisitor.visitLineNumber(12, label3);

    methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");

    methodVisitor.visitTypeInsn(NEW, "java/lang/StringBuilder");

    methodVisitor.visitInsn(DUP);

    methodVisitor.visitMethodInsn( INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);

    methodVisitor.visitLdcInsn("execute:");

    methodVisitor.visitMethodInsn( INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

    methodVisitor.visitVarInsn(LLOAD, 3);

    methodVisitor.visitVarInsn(LLOAD, 1);

    methodVisitor.visitInsn(LSUB);

    methodVisitor.visitMethodInsn( INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);

    methodVisitor.visitLdcInsn(" ms.");

    methodVisitor.visitMethodInsn( INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

    methodVisitor.visitMethodInsn( INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);

    methodVisitor.visitMethodInsn( INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

 

    //其他内容不需要处理

    Label label4 = new Label();

    methodVisitor.visitLabel(label4);

    methodVisitor.visitLineNumber(13, label4);

    methodVisitor.visitInsn(RETURN);

    Label label5 = new Label();

    methodVisitor.visitLabel(label5);

    methodVisitor.visitLocalVariable("args", "[Ljava/lang/String;", null, label0, label5, 0);

    methodVisitor.visitLocalVariable("start", "J", null, label1, label5, 1);

    methodVisitor.visitLocalVariable("end", "J", null, label3, label5, 3);

    methodVisitor.visitMaxs(6, 5);

    methodVisitor.visitEnd();

}

分别在方法的执行最前面和最后面插入相关的代码逻辑就好,这部分是辅助内容。

⑤创建方法访问者适配器

public class MethodAdapterVisitor extends AdviceAdapter {

    private String mClassName;

    private String mMethodName;

    private boolean mInject;

    private int mStart, mEnd;

    protected MethodAdapterVisitor(MethodVisitor mv, int access, String name, String desc, String className) {

        super(Opcodes.ASM5, mv, access, name, desc);

        mMethodName = name;

        this.mClassName = className;

    }

    //拦截注解方法

    @Override

    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {

        if ("Lcom/meishe/ms_asminject/MSTim eAnalysis;".equals(desc)) {

            mInject = true;

        }

        return super.visitAnnotation(desc, visible);

    }

    //方法进入

    @Override

    protected void onMethodEnter() {

        if (mInject) {

            //执行方法currentTimeMillis 得到startTime

            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);

            mStart = newLocal(Type.LONG_TYPE);

            mv.visitVarInsn(LSTORE, mStart);

        }

    }

    //方法结束

    @Override

    protected void onMethodExit(int opcode) {

        if (mInject) {

            //执行 currentTimeMillis 得到end time

            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);

            mEnd =newLocal(Type.LONG_TYPE);

            mv.visitVarInsn(LSTORE, mEnd);

            //得到静态成员 out

            mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");

            //new //class java/lang/StringBuilder

            mv.visitTypeInsn(NEW, "java/lang/StringBuilder");

            //引入类型 分配内存 并dup压入栈顶让下面的INVOKESPECIAL 知道执行谁的构造方法

            mv.visitInsn(DUP);

            //执行init方法 (构造方法)

            mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);

            //把常量压入栈顶

            mv.visitLdcInsn("execute "+ mMethodName +" :");

            //执行append方法,使用栈顶的值作为参数

            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);

            // 获得存储的本地变量

            mv.visitVarInsn(LLOAD, mEnd);

            mv.visitVarInsn(LLOAD, mStart);

            // lsub 减法指令

            mv.visitInsn(LSUB);

            //把减法结果append

            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);

            //执行StringBuilder 的toString方法

            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);

            //执行println方法

            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

        }

    }

}

继承AdviceAdapter,并重写相关的方法(org.objectweb.asm.commons.AdviceAdapter)是这个别继承错了。

visitAnnotation() 这个会输出方法拥有的注解信息,这里只对我们添加注解的方法进行注入操作。

onMethodEnter() 方法进入的时候会回调,在这里插入long start = System.currentTimeMillis();

onMethodExit(int opcode) 方法结束的时候回调,在这里插入:

long end = System.currentTimeMillis();

System.out.println(“execute:”+(end-start)+" ms.");

⑥上面用的注解

@Documented

@Target(ElementType.METHOD)

@Retention(RetentionPolicy.CLASS)

public @interface MSTimeAnalysis {

}

声明一个编译器注解即可,由于上边用到了,贴出来。

这样就完成了字节码插桩的全部工作。

⑧进行测试工作

public class MainActivity extends AppCompatActivity {

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        testApp();

    }

    @MSTimeAnalysis

    public void testApp(){

        try {

            Thread.sleep(1000);

        } catch (InterruptedException e) {

            e.printStackTrace();

        }

    }

}

声明一个testApp()方法,增加@MSTimeAnalysis方法,并调用。运行就可以看到输出:

2022-05-03 11:08:10.824 27788-27788/com.meishe.ms_asminject I/System.out: execute testApp :1000 ms.

在build/intermediates/transforms/ms_asm/debug/1/com/meishe/ms_asminject/MainActivity.class可以查看编译生成字节码:

protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    this.setContentView(2131427356);

    this.testApp();

}

@MSTimeAnalysis

public void testApp() {

    long var1 = System.currentTimeMillis();

    try {

        Thread.sleep(1000L);

    } catch (InterruptedException var6) {

        var6.printStackTrace();

    }

    long var4 = System.currentTimeMillis();

    System.out.println("execute testApp :" + (var4 - var1) + " ms.");

}

证明确实插桩成功了。  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值