理解一个工具的最快方式就是跑起来,然后原理自然了然于心 

本文以一个最简单的demo来实现对ASM全过程的了解。创建一个Child类,有一个call方法,最终的目的是在class类的call方法下增加一行输出语句。

ASM概念,操作流程:

需要创建一个 ClassReader 对象,将 .class 文件的内容读入到一个字节数组中

然后需要一个 ClassWriter 的对象将操作之后的字节码的字节数组回写

需要事件过滤器 ClassVisitor。在调用 ClassVisitor 的某些方法时会产生一个新的 XXXVisitor 对象,当我们需要修改对应的内容时只要实现自己的 XXXVisitor 并返回就可以了

1.引入最新的依赖:

//ASM相关依赖
implementation 'org.ow2.asm:asm:9.2'
implementation 'org.ow2.asm:asm-commons:9.1'

2.创建Child类,有一个phone属性,以及call()方法

public class Child {
    public String phone;
    public void call() {
        System.out.println("call");
    }
}

最终目的是在方法下面加一句,输入手机的名字和价格:

System.out.println("use :" + this.phone + " with price :" + this.price + " call");

3.创建方法过滤器ChildMethod,这里没有用MethodVisitor,使用了AdviceAdapter,因为它是 MethodVisitor 的子类,功能更全。

public class ChildMethod extends AdviceAdapter implements Opcodes {

    public ChildMethod(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
        super(api, methodVisitor, access, name, descriptor);
    }

    @Override
    public void visitCode() {
        //表示 ASM 开始扫描这个方法
        super.visitCode();
    }

    @Override
    public void visitEnd() {
        //表示方法扫码完毕
        super.visitEnd();
    }

    @Override
    protected void onMethodEnter() {
        //进入这个方法
        super.onMethodEnter();
    }

    @Override
    protected void onMethodExit(int opcode) {
        //即将从这个方法出去
        super.onMethodExit(opcode);
        Label label1 = new Label();
        mv.visitLabel(label1);
        mv.visitLineNumber(16, label1);
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
        mv.visitInsn(DUP);
        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
        mv.visitLdcInsn("use :");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, "com/ng/ngstatistical/test/asmhook/Child", "phone", "Ljava/lang/String;");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        mv.visitLdcInsn(" with price :");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, "com/ng/ngstatistical/test/asmhook/Child", "price", "Ljava/lang/String;");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        mv.visitLdcInsn(" call");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
    }

    @Override
    public void visitInsn(int opcode) {
        //扫描Opcodes操作符
        super.visitInsn(opcode);
    }
}

可以看到,添加小小的一行源代码却需要我们编写这么多的字节码,其实这里有一个取巧的办法,就是先在Child.java类中输入代码,实现我们想要的效果,再使用ASM Bytecode Viewer 插件来看生成的字节码,如下图所示:

android asm 统计时长 asm 安卓_ASM入门

 

之后,再复制到我们的方法过滤器的指定位置就好了~如此简单~高效~快捷~

android asm 统计时长 asm 安卓_android_02

4.然后需要实现类过滤器,在类过滤器中按方法名过滤方法,然后调用我们刚刚实现的方法过滤器:

public class ChildClassVisitor extends ClassVisitor {

    /**
     * @param api asm的api版本
     */
    public ChildClassVisitor(int api) {
        super(api);
    }

    public ChildClassVisitor(int api, ClassVisitor classVisitor) {
        super(api, classVisitor);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
        if (name.equals("call")) {
            return new ChildMethod(Opcodes.ASM9,methodVisitor,access,name,descriptor);
        }
        return methodVisitor;
    }
}

5.最后需要新创建一个类,编写main函数,首先需要调用一下Child类的call方法来生成class文件:

//生成class
private static void testChild() {
    Child child = new Child();
    child.call();
}

 public static void main(String[] args) {
        testChild();
        //startHook();
    }

运行之后,可以看到生成的class:

 

android asm 统计时长 asm 安卓_ASM入门_03

6.最后在main函数执行asm的调度方法:

//Child 的 class文件路径
    public static final String LOCAL_PATH = "/Users/xiaoguagua/AndroidProjects/MyProjects/ng_projects/NgStatistical/app/build/intermediates/javac/debug/classes/com/ng/ngstatistical/test/asmhook";

    private static void startHook() {
        try {
            //1.首先创建ClassReader,读取目标类Child的内容
            ClassReader cr = new ClassReader(Child.class.getName());
            //2.然后创建ClassWriter对象,
            ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
            ClassVisitor cv = new ChildClassVisitor(ASM9, cw);
            cr.accept(cv, Opcodes.ASM9);
            // 获取生成的class文件对应的二进制流
            byte[] code = cw.toByteArray();
            //将二进制流写到out/下
            FileOutputStream fos = new FileOutputStream(LOCAL_PATH + "/Child.class");
            fos.write(code);
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

运行,再去看刚才的Child.class文件,发现,它已经被改掉了,成功~

android asm 统计时长 asm 安卓_android asm 统计时长_04