Android ASM字节码插桩(转载)

377 阅读16分钟

版权声明:本文为CSDN博主「孟芳芳」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:blog.csdn.net/zenmela2011…

(如有侵权,请联系删除)

1.ASM

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

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

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

截屏2023-06-06 下午6.11.27.png

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

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

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

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

2.ASM的使用

①引入ASM依赖: 截屏2023-06-06 下午6.09.48.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查看字节码指令。也可以借助插件来查看,就不需要我们手动执行各种命令。

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

点击ASM Bytecode Viewer之后会弹出

所以第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中的格式如下:

        对应的代码如下:

        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方法可以判断每个方法的注解,然后在每个方法的onMethodEnteronMethodExit里根据是否有这个注解来判断是否需要执行插桩。

        修改后,插桩生成的的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工程的构建过程:

        ①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.");

        }

        证明确实插桩成功了。