字节码插桩-Gradle Plugin+Transform+ASM实战

275 阅读15分钟

AOP

AOP 是 Aspect Oriented Programming 的缩写,译为面向切向编程。AOP和OOP(Object Oriented Programming,面向对象编程)是不同的编程思想。OOP 是把问题划分到单个模块,AOP 是把涉及到众多模块的某一类问题进行统一管理。AOP的目标是把某个功能集中起来,放到一个统一的地方来控制和管理。利用 AOP 思想,对业务逻辑的各个部分进行了隔离,从而降低业务逻辑各部分之间的耦合,提高程序的可重用性,提高开发效率。

常用的AOP工具:

OOP方式

  1. APT:是一种处理注解的工具,可以在编译期帮我们生成Java文件(需要手动拼接代码,或使用Javapoet),但无法修改已有Java文件,应用案例:ButterKnife、Dragger2、EventBus3、DataBinding
  2. AspectJ:可以修改Java文件和Class字节码,功能强大,最常见的AOP库,但是存在切入点固定和性能较低等缺点。应用案例:Hugo、AspectJx
  3. Javassist:它是一个编辑Class字节码的类库,编程简单,直接使用Java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构或者动态生成类。应用案例:HotFix
  4. ASM:可以修改Class字节码,在编译时插入逻辑,性能好,有ASM Btyecode Viewer等插件支持生成ASM代码。应用案例:booster、lancet等框架
  5. ASMDEX、DexMaker:也是静态织入代码,学习成本太高
  6. cglib:运行时织入代码,作用于class字节码,常用的动态代理库,比JVM自带的动态代理更灵活,但不适用于Android,因为Android运行时是dex文件,不是class文件
  7. xposed、dexposed、epic:运行时hook,有兼容性问题,只适合调试时玩玩,不适合生产环境。

常用的编译期静态织入AOP方式:APT、AspectJ、Javassist、ASM。

字节码插桩:

字节码插桩就是在.class文件转为.dex之前,修改.class文件从而达到修改代码的目的。常用的框架有:AspectJ、Javassist、ASM。

AspectJ 框架虽然比较成熟并且使用简单的优点,但是存在切入点固定和性能较低等缺点。

目前主流的字节码修改框架是ASM和Javaassist,两者对比:

特性JavassistASM
包大小771 KB (3.27)265 KB (6.0 BETA)
性能劣于 ASM优于 Javassist
API 封装程度
功能完备程度完备完备
对开发者的要求基本了解 class 文件格式和 JVM 指令集需要精通 class 文件格式和 JVM 指令集
学习曲线平缓陡峭
文档及手册简单明了有些繁琐(Vistor 模式让初学者有点懵)

从上面的对比来看,如果是初学者,建议选择 Javassist,毕竟上手快,学习起来比较容易,如果是对性能、包体积方面要求比较高,建议选择 ASM。Android的相关开源框架中,使用ASM较多。我们下面选择的也是ASM。

在下面的介绍中,介绍常用的编译时插桩的方式为:Gradle Plugin+Transform+ASM。

应用场景:

处理三方库的代码:当某些第三方 SDK 库没有源码,而想对它的class直接做点手脚;或者给它内部的一个崩溃函数增加 try catch,实现”异常处理“。

性能监控:对一些class插桩,做UI,内存,网络等等方面的性能监控。

API代理:对隐私相关的API进行插桩代理,实现隐私合规。

以及权限控制、无痕埋点、安全控制、日志记录、事件防抖、大图检测、多线程优化等等。

ASM的使用

做项目优化时,我们通常会先打印出方法的执行时间,再根据方法的耗时情况对其进行优化。代码如下:

public static void main(String[] args) {
        long start = System.currentTimeMillis(); 
        //...
        long end = System.currentTimeMillis(); 
        System.out.println("execute:" + (end - start));
}

如果是一两个方法我们手动插入代码没有问题,但是整个项目都要我们手动去插入的话,那会把手废掉的。接下来就以使用字节码插桩的方式,批量添加方法的耗时打印。

1、引入ASM

首先找到ASM的官网:asm.ow2.io/。可以找到最新的版本ASM 9.3(成稿时)。

因此,我们可以在app中build.gradle中的dependencies加入:

 testImplementation('org.ow2.asm:asm:9.3')
 testImplementation('org.ow2.asm:asm-commons:9.3')//一些通用封装,使用更方便

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

2、准备待插桩Class

在工程中创建一个Java类:

public class InjectMethod {
    @MethodTime
    public static void main(String[] args) {
        System.out.println("my words");
    }
}

由于我们操作的是字节码插桩,所以先执行build让这个类进行编译生成对应的class文件。在{projectDir}/build/intermediates/javac/debug/classes/{pakagename}/路径下会有InjectMethod.class文件。

创建MethodTime注解类:

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD})
public @interface MethodTime {
}

创建此注解类后,给指定的方法加上注解,会在接下来只插桩被此注解的方法,而不是插桩所有的方法。

3、执行插桩

我们输入命令:java InjectMethod执行这个Class只会输出“my words”,没有执行时间的log输出。那么我们接下来利用ASM,向main方法中插入记录函数执行时间的日志输出。

在单元测试中(test/java目录中)写入测试方法:

public class Test {
​
    @org.junit.Test
    public void test() throws IOException {
        /**
         * 1、准备待分析的class
         */
        FileInputStream fis = new FileInputStream
                ("{projectDir}/build/intermediates/javac/debug/classes/{pakagename}/InjectMethod.class");
​
        /**
         * 2、执行分析与插桩
         */
        //用于读取已经编译好的 .class 文件
        ClassReader classReader = new ClassReader(fis);
        // 用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件
        // COMPUTE_MAXS 告诉ASM 自动计算栈的最大值以及最大数量的方法的本地变量。
        // COMPUTE_FRAMES 标识让ASM 自动计算方法的栈桢
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        // 分析,使ClassAdapterVisitor访问类文件结构,处理结果写入classWriter,
        // EXPAND_FRAMES:说明在读取 class 的时候需要同时展开栈映射帧(StackMap Frame)
        classReader.accept(new ClassAdapterVisitor(classWriter), ClassReader.EXPAND_FRAMES);
​
        /**
         * 3、获得结果并输出
         */
        String pathName = "{projectDir}/src/test/result/";
        byte[] newClassBytes = classWriter.toByteArray();
        File file = new File(pathName);
        file.mkdirs();
        FileOutputStream fos = new FileOutputStream
                (pathName + "InjectMethod.class");
        fos.write(newClassBytes);
        fos.close();
    }
}

上面的代码会获取上一步生成的class,然后由ASM执行完插桩之后,将结果输出到test/result目录下。

第1步是读取class文件,第3步是将最终的文件保存,最主要的是第2步的处理流程。

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

public class ClassAdapterVisitor extends ClassVisitor {
​
    public ClassAdapterVisitor(ClassVisitor cv) {
        super(Opcodes.ASM9, cv);
    }
​
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature,
                                     String[] exceptions) {
        System.out.println("method's name:" + name + " desc:" + desc);
        MethodVisitor mv = super.visitMethod(access, name, desc, signature,
                exceptions);
        return new MethodAdapterVisitor(api, mv, access, name, desc);
//        return new MethodAdapterVisitorByAsmByteCoderViewer(api, mv, access, name, desc);
    }
}

我们只关注class的method插桩。所以,此处只覆写了visitMethod方法。在这个方法中我们返回一个MethodVisitor方法分析器对象。一个方法的参数、注解以及方法体需要在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);
    }
​
​
    /**
     * 分析方法上面的注解
     * 判断当前这个方法是不是使用了MethodTime,如果使用了,我们就需要对这个方法插桩
     */
    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        System.out.println("desc: " + desc);
        if (Type.getDescriptor(MethodTime.class).equals(desc)) {
            System.out.println(desc);
            inject = true;
        }
        return super.visitAnnotation(desc, visible);
    }
​
    private int start;
​
    /**
     * 方法进入的时候执行
     */
    @Override
    protected void onMethodEnter() {
        super.onMethodEnter();
        System.out.println("onMethodEnter inject: " + inject);
        if (inject) {
            //invokeStatic指令,调用静态方法
            invokeStatic(Type.getType("Ljava/lang/System;"),
                    new Method("currentTimeMillis", "()J"));
            //创建本地 LONG类型变量
            start = newLocal(Type.LONG_TYPE); //创建本地 LONG类型变量
            System.out.println("start: " + start);
            //记录 方法执行结果给创建的本地变量
            //store指令 将方法执行结果从操作数栈存储到局部变量
            storeLocal(start);
        }
    }
​
    /**
     * 方法返回的时候执行
     *
     * @param opcode
     */
    @Override
    protected void onMethodExit(int opcode) {
        super.onMethodExit(opcode);
        System.out.println("onMethodExit inject: " + inject);
        if (inject) {
            invokeStatic(Type.getType("Ljava/lang/System;"),
                    new Method("currentTimeMillis", "()J"));
            int end = newLocal(Type.LONG_TYPE);
            //store指令 将方法执行结果从操作数栈存储到局部变量
            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(asm-common库中的封装对象),其实就是MethodVisitor 的子类,AdviceAdapter封装了指令操作的方法,更为直观与简单。

visitAnnotation中判断此方法是否被MethodTime注解了,只有被注解的,才会对方法插桩。

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

onMethodExit在退出一个方法时候回调,我们需要在这个方法中插入long end = System.currentTimeMillis(); System.out.println("execute:" + (end - start));;

这两部分的代码怎么写呢?这就需要有一些字节码的知识了。字节码知识不是本文的重点,再次不展开描述。字节码的相关知识,可以上网查找资料。

接下来我们介绍两种查看字节码的方式:

第一种:使用javap查看字节码:

首先创建一个目标Java类:

public class TargetMethod {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        System.out.println("my words");
        long end = System.currentTimeMillis();
        System.out.println("execute:" + (end - start));
    }
}

将java编译为class文件:javac TargetMethod.java

查看字节码:javap -c TargetMethod.class

可以得到下面的字节码命令:

javap查看字节码

对应关系为:

1:long start = System.currentTimeMillis();

2: System.out.println("my words");

3:long end = System.currentTimeMillis(); System.out.println("execute:" + (end - start));

2是原来方法中的代码,不需要处理,只需要关注1和3处的字节码。

所以通过字节码与ASM API的对应关系,可以写出MethodAdapterVisitor中的onMethodEnteronMethodExitASM插桩代码。

第二种:使用ASM Bytecode Viewer插件

既然通过字节码这种方式这么麻烦,还需要对照字节码,通过ASM的方法自己实现插桩的逻辑,有没有更简单的方法呢?答案是有的。就是借助插件ASM Bytecode Viewer查看。

AS安装ASM Bytecode Viewer后,找到{projectDir}/build/intermediates/javac/debug/classes/{pakagename}/路径下的TargetMethod.class文件。在class类源码中点击右键,选择ASM Bytecode Viewer,会弹出ASM Plugin界面。

在此界面的上面会有三项内容:

  • Bytecode 表示对应的class字节码文件
  • ASMified 表示利用ASM框架生成字节码对应的代码
  • Groovified 对应的是class字节码指令

Bytecode中也可以类似使用javap查看字节码。我们主要看ASMified中的代码,这里面的代码就是ASM的插桩代码。将TargetMethod.class与InjectMethod.class中展示的ASMified代码对比之后,可以得到如下的代码:

public class MethodAdapterVisitorByAsmByteCoderViewer extends AdviceAdapter {
​
    private boolean inject;
private static final String ANNOTATION_TRACK_METHOD = "Lcom/example/asmdemo/MethodTime;";
​
    protected MethodAdapterVisitorByAsmByteCoderViewer(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
        super(api, methodVisitor, access, name, descriptor);
    }
​
​
    /**
     * 分析方法上面的注解
     * 判断当前这个方法是不是使用了ASMTest,如果使用了,我们就需要对这个方法插桩
     */
    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        System.out.println("desc: " + desc);
        if (ANNOTATION_TRACK_METHOD.equals(desc)) {
            System.out.println(desc);
            inject = true;
        }
        return super.visitAnnotation(desc, visible);
    }
​
    private int start;
​
    /**
     * 方法进入的时候执行
     */
    @Override
    protected void onMethodEnter() {
        super.onMethodEnter();
        System.out.println("onMethodEnter inject: " + inject);
        if (inject) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            //存储局部变量1
            mv.visitVarInsn(LSTORE, 1);
        }
    }
​
    /**
     * 方法返回的时候执行
     *
     * @param opcode
     */
    @Override
    protected void onMethodExit(int opcode) {
        super.onMethodExit(opcode);
        System.out.println("onMethodExit inject: " + inject);
        if (inject) {
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            //存储局部变量3
            mv.visitVarInsn(LSTORE, 3);
            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("execute:");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
​
            //减法
            mv.visitVarInsn(LLOAD, 3);
            mv.visitVarInsn(LLOAD, 1);
            mv.visitInsn(LSUB);
​
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)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);
        }
    }
}

在ASM当中,Label类可以用于实现选择(if、switch)、循环(for、while)和try-catch语句。此处只是简单的顺序结构,不需要跳转的逻辑。所以,上面代码删除了Label相关的代码。

将ClassAdapterVisitor中visitMethod中的 return new MethodAdapterVisitor(api, mv, access, name, desc);替换为 return new MethodAdapterVisitorByAsmByteCoderViewer(api, mv, access, name, desc);。可以实现同样的效果。

使用这种方式,只需要了解少量的字节码知识,就可以实现ASM插桩功能。

4、生成插桩之后的class

通过上面的分析过程,已经完成对注解方法的字节码插桩的实现。运行Test的方法,会在test/result目录下生成以下class类:

public class InjectMethod {
    public InjectMethod() {
    }
​
    @MethodTime
    public static void main(String[] var0) {
        long var1 = System.currentTimeMillis();
        System.out.println("my words");
        long var3 = System.currentTimeMillis();
        System.out.println("execute:" + (var3 - var1));
    }
}

这就完成了对此方法的插桩实现。

Transform的使用

通过上面的介绍,已经具备对字节码修改的能力了,但是想要在编译过程中自动对字节码修改,还需要引入Gradle Transform的相关知识。

android详细打包流程

上面这张图片描述的apk编译流程对Android开发者来说,应该是比较熟悉的。我们主要关注的是将class文件打包成dex文件之前的这个阶段(如图中箭头处所示),中间就涉及到Android Gradle Plugin的Transform流程。

Transform 是由Android Gradle Plugin提供,允许第三方插件在将已编译的类文件转换为dex文件之前对其进行操作。

Transform原理图如下所示:

Transform

将class文件、jar文件、资源文件作为输入,经过一系列的Transform处理,首先是自定义的Transform处理,然后是系统的Transform处理,最后一个Transform是负责生成dex文件。

可以通过自定义Plugin,注册一个自定义的Transform到编译流程中去,目的是拿到所有.class文件,再结合ASM 工具修改字节码。

自定义Plugin

使用buildSrc的方式自定义Plugin(自定义的Plugin的作用范围是整个工程),介入到编译流程中。

为了能够引入Gradle的能力,将buildSrc内的build.gradle的内容修改成如下的形式。

apply plugin: 'groovy'
​
dependencies {
    implementation gradleApi()//gradle sdk
​
    implementation 'com.android.tools.build:gradle:3.5.4'
    implementation 'com.android.tools.build:gradle-api:3.5.4'
​
    //ASM依赖
    implementation 'org.ow2.asm:asm:9.3'
    implementation 'org.ow2.asm:asm-util:9.3'
    implementation 'org.ow2.asm:asm-commons:9.3'
}
​
repositories {
    google()
    jcenter()
}

上述内容完成sync以后,然后在java下自定义Plugin:

public class AsmPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        System.out.println("===========  AsmPlugin  ============");
        AppExtension appExtension = project.getExtensions().findByType(AppExtension.class);
        assert appExtension != null;
        //注册自定义Transform
        appExtension.registerTransform(new AsmTransform(project));
    }
}

自定义Transform

public class AsmTransform extends Transform {
​
    Project project;
​
    public AsmTransform(Project project) {
        this.project = project;
    }
​
​
    /**
     * 获取Transform的名字
     */
    @Override
    public String getName() {
        return AsmTransform.class.getName();
    }
​
​
    /**
     * 需要处理的数据类型,有两种枚举类型
     * CONTENT_CLASS:编译后的字节码文件(jar 包或目录)
     * CONTENT_RESOURCES:标准的 Java 资源
     */
    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }
​
​
    /**
     * 指 Transform 要操作内容的范围,官方文档 Scope 有 7 种类型:
     * 1. EXTERNAL_LIBRARIES        只有外部库
     * 2. PROJECT                   只有项目内容
     * 3. PROJECT_LOCAL_DEPS        只有项目的本地依赖(本地jar)
     * 4. PROVIDED_ONLY             只提供本地或远程依赖项
     * 5. SUB_PROJECTS              只有子项目。
     * 6. SUB_PROJECTS_LOCAL_DEPS   只有子项目的本地依赖项(本地jar)。
     * 7. TESTED_CODE               由当前变量(包括依赖项)测试的代码
     */
    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }
​
    /**
     * 是否支持增量更新
     */
    @Override
    public boolean isIncremental() {
        return true;
    }
​
    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation);
        //输入,可以从中获取jar包和class文件夹路径。需要输出给下一个任务
        Collection<TransformInput> inputs = transformInvocation.getInputs();
        //OutputProvider管理输出路径
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
        //遍历目录
        for (TransformInput input : inputs) {
            // 遍历jar 第三方引入的 class
            for (JarInput jarInput : input.getJarInputs()) {
                File dest = outputProvider.getContentLocation(
                        jarInput.getFile().getAbsolutePath(),
                        jarInput.getContentTypes(),
                        jarInput.getScopes(),
                        Format.JAR);
                //不处理jar文件,直接copy
                FileUtils.copyFile(jarInput.getFile(), dest);
            }
            //
            for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
                handleDirectoryInput(directoryInput, outputProvider);
            }
        }
    }
​
​
    private static void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) throws IOException {
        //是否是目录
        if (directoryInput.getFile().isDirectory()) {
            File dest = outputProvider.getContentLocation(directoryInput.getName(),
                    directoryInput.getContentTypes(), directoryInput.getScopes(),
                    Format.DIRECTORY);
            transformDir(directoryInput.getFile(), dest);
        }
    }
​
​
    private static void transformDir(File input, File dest) throws IOException {
        if (dest.exists()) {
            FileUtils.forceDelete(dest);
        }
        FileUtils.forceMkdir(dest);
        String srcDirPath = input.getAbsolutePath();
        String destDirPath = dest.getAbsolutePath();
        for (File file : Objects.requireNonNull(input.listFiles())) {
            String destFilePath = file.getAbsolutePath().replace(srcDirPath, destDirPath);
            File destFile = new File(destFilePath);
            if (file.isDirectory()) {
                transformDir(file, destFile);
            } else if (file.isFile()) {
                if (canHandle(file.getName())) {
                    FileUtils.touch(destFile);
                    weave(file.getAbsolutePath(), destFile.getAbsolutePath());
                }
            }
        }
    }
​
​
    private static void weave(String inputPath, String outputPath) {
        try {
            FileInputStream is = new FileInputStream(inputPath);
            ClassReader cr = new ClassReader(is);
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
            ClassAdapterVisitor adapter = new ClassAdapterVisitor(cw);
            cr.accept(adapter, ClassReader.EXPAND_FRAMES);
            FileOutputStream fos = new FileOutputStream(outputPath);
            fos.write(cw.toByteArray());
            fos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
​
    /**
     * 检查class文件是否需要处理
     */
    static boolean canHandle(String name) {
        return (name.endsWith(".class")
                && !name.startsWith("R\$")
                && !"R.class".equals(name)
                && !"BuildConfig.class".equals(name));
    }
}

可以看到weave方法中的实现和上面使用ASM读取、操作、写入字节码的功能是相同的。将上面使用ASM操作字节码的类,拷贝到此处,功能就完成了。如下:

buildSrc目录

自定义Plugin插件的功能完成后,需要在app project的build.gradle中引入自定义Plugin:

import com.example.asm.AsmPlugin
apply plugin: AsmPlugin

之后执行app project的build task。执行完成后,在(文件路径:app/build/intermediates/transform /{包名}/debug / {文件}/{包名})路径下,可以看到插桩成功的class文件。

其他:

目前也有一些开源的编译时插桩的库,例如饿了么开源的lancet,原理也是 Gradle Plugin+Transform+ASM。

如果想深入学习字节码插桩,推荐boosterHunterByteX,里面有好多字节码操作可以学习,例如大图监控,网络监控等等。

总结:

本文先介绍了AOP,然后引出了字节码插桩,对比了常用的框架。之后介绍了使用ASM进行字节码插桩的实现方式,然后结合Transform实现了Android编译过程中对字节码的插桩。

参考链接:

www.jianshu.com/p/0799aa19a…

github.com/didi/booste…

www.mdnice.com/writing/eac…

time.geekbang.org/column/arti…

juejin.cn/post/689194…

cloud.tencent.com/developer/a…

juejin.cn/post/686327…

juejin.cn/post/704339…