一步步实现AddTryCatch插件 —— Gradle Transform和ASM实践

81 篇文章 3 订阅
19 篇文章 0 订阅

在上一篇AddTryCatch gradle plugin 编译期给代码加try catch的插件中介绍了AddTryCatch插件的使用方法,这次我们来一步步实现一下AddTryCatch插件。

参考这篇文章,可以快速开发一个gradle插件,记录了我很多的探索和踩坑,看完这篇文章直接就可以上手开发插件了,绝对比网上其他绝大多数教程介绍的步骤要简单的多。

本文中
1. 使用了java开发,而不是网上清一色的groovy,没有语言学习门槛;
2. 使用buildSrc目录开发,不需要创建单独的plugin module,也省去了测试的时候要先发布的麻烦;
3. 使用java-gradle-plugin插件,所有配置都写在gradle文件里,不需要单独创建resources/META-INF/gradle-plugins/pluginName.properties配置文件
吐槽一句:真是天下文章一大抄,网上的gradle插件开发文章,全是几年前的旧开发模式,互相抄来抄去,学不到新东西。一开始我还以为gradle插件开发真的很难,后来仔细看了gradle的官网文档,才发现,现在开发gradle插件已经非常容易了。

先附上源码地址:https://github.com/xingchenxuanfeng/AddTryCatchPlugin

插件原理

先讲一下插件原理:
利用gralde transform api在编译流程中加入自定义的transform任务,然后在自定义的transform任务中,使用asm修改字节码来达到注入try catch代码的目的。同时使用Hunter框架来优化transform任务运行效率,简化代码逻辑。

ASM

ASM是一个通用的Java字节码操作和分析框架。它可以用于修改现有类或直接以二进制形式动态生成类。ASM提供了一些常见的字节码转换和分析算法,可以从中构建自定义复杂转换和代码分析工具。ASM提供与其他Java字节码框架类似的功能,但专注于性能。因为它的设计和实现尽可能小而且快,所以它非常适合在动态系统中使用(当然也可以以静态方式使用,例如在编译器中)。
目前已广泛应用于众多著名项目,如OpenJDK,Groovy编译器,Kotlin编译器Gradle等。

Gradle transform api

Gradle Transform是Android官方提供给开发者在项目构建阶段中由class到dex转换之前修改class文件的一套api。目前比较经典的应用是字节码插桩、代码注入技术。

Hunter

Hunter: 一个插件框架,在它的基础上可以快速开发一个并发、增量的字节码编译插件,帮助开发人员隐藏了Transform和ASM的绝大部分逻辑,开发者只需写少量的ASM code,就可以开发一款编译插件,修改Android项目的字节码。

Hunter这个框架实在是很棒,必须要给这个项目点个赞!通过使用这个框架,节省了我很多啰嗦的gradle transform代码,让我可以把更多的精力放到实际的功能开发上来(如果是第一次开发Gradle Transform插件,光是这部分啰嗦的代码,估计就得花费大半天时间)。同时使用了该框架,还能自动利用增量编译、并发编译优化性能,在我的工作项目中,AddTryCatch插件和另一个没有使用Hunter框架开发的类似Gradle插件比起来,速度快了近10倍!


具体实现

一 建立插件module

一般开发Gradle插件,项目中至少有两个module:一是app module,用来写demo做测试用,二是plugin module,实现具体的插件。
我们这里不建plugin module,直接建一个名称为buildSrc的目录,这个是gradle默认的gradle插件源码module。使用buildSrc名称的好处在于,gradle在编译任意module前会先编译buildSrc modlue,并将编译产物放入gradle的环境变量中,用于被其他module的编译任务所引用。

在buildSrc目录下新建一个build.gradle配置文件,然后输入如下代码:

apply plugin: 'java-gradle-plugin' //应用java-gradle-plugin插件

gradlePlugin {
    plugins {
        addTryCatchPlugin { //transform task的名字,随便起的
            id = 'add-trycatch' //插件的id,与应用插件时写的 apply plugin: 'add-trycatch' 名字要一致
            implementationClass = 'com.addtrycatch.AddTryCatchPlugin' //插件具体实现类的类名
        }
    }
}

dependencies {
    implementation 'com.android.tools.build:gradle:3.0.1' //应用的gradle版本
    implementation('com.quinn.hunter:hunter-transform:0.9.2') { //使用hunter框架
        exclude group: 'com.android.tools.build' //排除hunter带来的gradle传递依赖,以便自定义应用的gradle版本
    }
}

repositories {
    google()
    jcenter()
    mavenCentral()
}

以上就是插件的gradle配置,每个参数含义我都写了注释,需要自己写插件的时候,复制到自己项目中,参考注释配置即可。

二 编写插件入口

和开发android或者java工程一样,开发插件的源码目录也是src/main/java。如果gradle没有自动创建这个目录,可以手动建一下。然后在其下新建插件类AddTryCatchPlugin

public class AddTryCatchPlugin implements Plugin<Project> {

    @Override
    public void apply(Project project) {
        AddTryCatchExtension extension = project.getExtensions().create("addTryCatch", AddTryCatchExtension.class);
        Config.getInstance().extension = extension;
        
        AppExtension appExtension = (AppExtension) project.getProperties().get("android");
        appExtension.registerTransform(new AddTryCatchTransform(project), Collections.EMPTY_LIST);
    }
}

project.getExtensions().create("addTryCatch", AddTryCatchExtension.class)表示从使用者的gradle配置中读取插件配置信息,可让插件的使用者可以自定义一些插件的属性。

AddTryCatchExtension类是一个普通的javaBean,数据结构如下。

public class AddTryCatchExtension {
    public Map<String, List<String>> hookPoint;
    public Map<String, String> exceptionHandler;
}

使用的时候按如下形式配置,写在build.gradle文件中。即可与上面的AddTryCatchExtension类对应上。

addTryCatch {
    hookPoint = ["com.addtrycatchplugin.TestCrash1" : [
                    "crashMethod1","crashMethod2"]]
    exceptionHandler = ["com.addtrycatchplugin.ExceptionUtils": "uploadCatchedException"]
}

project.getExtensions().create("addTryCatch", AddTryCatchExtension.class)从build.gradle中解析对应的addTryCatch块,然后生成一个AddTryCatchExtension实例。并保存在了Config.getInstance().extension中,以便后面使用。

Config类是一个自定义的单例,用来保存各个地方都可能需要读取的配置,这里只保存了一个AddTryCatchExtension

public class Config {
    private static Config sInstance = new Config();

    public AddTryCatchExtension extension;

    public static Config getInstance() {
        return sInstance;
    }
}

AppExtension appExtension = (AppExtension) project.getProperties().get("android"); appExtension.registerTransform(new AddTryCatchTransform(project), Collections.EMPTY_LIST);这两行就是实例化了一个自定义Transform类AddTryCatchTransform,并注册到编译流程中去。

三 编写自定义Transform类

这里使用Hunter框架来写Transform类,非常简单,不需要像传统写Transform那样,自己写所有目录和jar包的遍历、拷贝或者移动。同时自动利用增量编译、并发编译优化性能,实在是不可多得的好框架。
ps:如果不使用Hunter框架的话,也可以参考Transform详解,来自己实现相关Transform代码。

代码如下:

class AddTryCatchTransform extends HunterTransform {

    public AddTryCatchTransform(Project project) {
        super(project);
        this.bytecodeWeaver = new AddTryCatchWeaver();
    }
}

只要继承HunterTransform类,然后在构造方法里给this.bytecodeWeaver赋值一个AddTryCatchWeaver的实例。

AddTryCatchWeaver的代码如下:

class AddTryCatchWeaver extends BaseWeaver {
    @Override
    public boolean isWeavableClass(String fullQualifiedClassName) {
        return Config.getInstance().extension.hookPoint.containsKey(fullQualifiedClassName.replace(".class", ""));
    }

    @Override
    protected ClassVisitor wrapClassWriter(ClassWriter classWriter) {
        return new AddTryCatchClassAdapter(classWriter);
    }
}

AddTryCatchWeaver继承BaseWeaver,重写了两个方法。

  1. isWeavableClass方法,表示是否需要对该类进行处理。该transform任务会遍历所有的class,在遍历每一个class的时候都会调用该方法来判断是否要对该类进行处理。该方法的参数是类的全类名,我们这里判断了类名是否在配置文件里存在,存在的话就处理。
  2. wrapClassWriter方法返回具体处理类字节码的ClassVisitor,这里返回了一个我们自定义的AddTryCatchClassAdapter类。

以上,我们已经完成了基本框架的编写,下面就是具体处理类字节码了。

四 编写自定义ClassVisitor

Hunter框架会自动帮我们处理class字节码的输入输出,我们只要在wrapClassWriter方法返回的自定义ClassVisitor类中处理字节码即可。

ClassVisitor是ASM框架提供的一个抽象类,我们可以通过实现该类来便捷的修改字节码,而无需关心字节偏移、class 文件的校验码等相对复杂的过程。
在ASM中,class文件被描述成树的结构,扫描class文件的过程中,会有顺序的调用visitvisitAnnotationvisitFieldvisitMethodvisitEnd等方法。重写这几个方法,即可做到对字节码进行读取、修改。

有关ASM的知识,下面这篇文章写得很好,有兴趣的话可以看一下。
AOP 的利器:ASM 3.0 介绍

ASMifier

ASM的api学习起来有一定难度,我也不是很熟悉,但是这并不妨碍我们的开发。因为我们有ASMifier插件!在Android Studio插件中搜索ASM Bytecode Outline 2017下载即可。

这个工具的作用是,显示生成对应的字节码所需的ASM代码,这对于不太熟悉ASM api的人来说帮助很大。

找到Try Catch对应的ASM代码

如下是我的使用方法:

  1. 先在设置里找到ASM Bytecode Outline的设置项,然后把Skip debug和Skip frames两项勾选上,这样可以减少部分不必要的ASM代码。
  2. 我们先写一个不带try catch的方法,然后在类上点击右键,在弹出菜单里点击Show Bytecode outline开始生成对应的字节码,再点击弹出框上的ASMified按钮,即可显示生成我们当前类所需的ASM代码。
  3. 然后我们在该方法里加上try catch,再次重复第二步生成ASM代码,然后点击Show differences按钮,即可比较这次生成的ASM代码和上次生成的ASM的区别。分析这个区别,即可找到添加try catch的部分代码是什么。

我通过上述方式找到了需要生成try catch字节码的ASM代码如下:

Label l0 = new Label();
Label l1 = new Label();
Label l2 = new Label();
mv.visitTryCatchBlock(l0, l1, l2, "java/lang/Exception");
......
mv.visitLabel(l1);
Label l3 = new Label();
mv.visitJumpInsn(GOTO, l3);
mv.visitLabel(l2);
mv.visitVarInsn(ASTORE, 1);
mv.visitLabel(l3);
实现具体的ClassAdapter和MethodVisitor

下面我们继续实现AddTryCatchClassAdapter,这个类是修改字节码的入口。但是我们并不需要修改全部类,仅仅需要修改类中的某个方法,所以我们又实现了一个MethodVisitor来单独做方法的修改,让代码更容易编写和维护,在AddTryCatchClassAdapter中再调用MethodVisitor来做方法的修改。

AddTryCatchClassAdapter代码如下:

class AddTryCatchClassAdapter extends ClassVisitor {
    private String mClassName;
    private List<String> mMethodNames;

    public AddTryCatchClassAdapter(ClassWriter classWriter) {
        super(Opcodes.ASM5, classWriter);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        mClassName = name.replace("/", ".");
        mMethodNames = Config.getInstance().extension.hookPoint.get(mClassName);
        super.visit(version, access, name, signature, superName, interfaces);
        System.out.println("add try catch class :" + mClassName);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        if (mMethodNames.contains(name)) {
            return new AddTryCatchAdviceAdapter(Opcodes.ASM5,
                    super.visitMethod(access, name, desc, signature, exceptions), access, name, desc);
        }
        return super.visitMethod(access, name, desc, signature, exceptions);
    }
}

我们在visit方法中读取当前要处理的类名,然后根据配置来找到当前类中需要加try catch的方法。在visitMethod方法中,判断当前方法是要加try catch的方法就返回自定义的AddTryCatchAdviceAdapter类的实例,否则返回super方法。

AddTryCatchAdviceAdapter的实现如下

public class AddTryCatchAdviceAdapter extends AdviceAdapter {

    Label l1;
    Label l2;
    private String exceptionHandleClass;
    private String exceptionHandleMethod;

    protected AddTryCatchAdviceAdapter(int api, MethodVisitor mv, int access, String name, String desc) {
        super(api, mv, access, name, desc);
        Map<String, String> exceptionHandler = Config.getInstance().extension.exceptionHandler;
        if (exceptionHandler != null && !exceptionHandler.isEmpty()) {
            exceptionHandler.entrySet().forEach(entry -> {
                exceptionHandleClass = entry.getKey().replace(".", "/");
                exceptionHandleMethod = entry.getValue();
            });
        }
    }

    @Override
    protected void onMethodEnter() {
        super.onMethodEnter();
        Label l0 = new Label();
        l1 = new Label();
        l2 = new Label();
        mv.visitTryCatchBlock(l0, l1, l2, "java/lang/Exception");
        mv.visitLabel(l0);
    }

    @Override
    protected void onMethodExit(int i) {
        super.onMethodExit(i);
        mv.visitLabel(l1);
        Label l3 = new Label();
        mv.visitJumpInsn(GOTO, l3);
        mv.visitLabel(l2);
        mv.visitVarInsn(ASTORE, 1);
        if (exceptionHandleClass != null && exceptionHandleMethod != null) {
            mv.visitVarInsn(ALOAD, 1);
            mv.visitMethodInsn(INVOKESTATIC, exceptionHandleClass,
                    exceptionHandleMethod, "(Ljava/lang/Exception;)V", false);
        }
        mv.visitLabel(l3);
    }
}

AdviceAdapter继承于MethodVisitor,是一个对MethodVisitor方便的封装,提供了onMethodEnteronMethodExit两个方法,分别表示扫描器进入方法和离开方法的时机。
我们自定义的AddTryCatchAdviceAdapter继承AdviceAdapter,并重写onMethodEnteronMethodExit两个方法来在该方法的第一行和最后一行插入我们需要的字节码。把我们前面用ASMifier插件得到的asm代码放到这里即可。

上述代码除了加入了try catch,还加入了触发异常后,catch代码块内对异常的处理部分。
mv.visitMethodInsn(INVOKESTATIC, exceptionHandleClass,exceptionHandleMethod, "(Ljava/lang/Exception;)V", false);表示执行一个静态方法,方法的类名是exceptionHandleClass,方法名是exceptionHandleMethod,参数类型是java.lang.Exception

五 发布

Gradle中常用的maven库有google、jcenter、mavenCentral等,我们这里选择最简单的,发布到jitpack.io上。
步骤非常简单:

  1. 在buildSrc module下的build.gradle中加入如下代码,直接复制就好,完全不用任何修改,然后把项目发布到自己的GitHub上。
...
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1'
    }
}
apply plugin: 'com.github.dcendents.android-maven'
  1. 在GitHub上,点击releases ——> Draft a new release来创建一个新的release版本。

  2. 到 https://jitpack.io/ 上,使用自己的GitHub账号登录,选择自己的仓库后,点击Look up按钮,再对应版本后面点击Get it按钮即可。等待后台构建完成,就发布成功了。

总结

以上,就完成了AddTryCatch插件的编写。

我们再总结一下所有的步骤:

  1. 建立buildSrc moudle,并在其中的build.gradle文件中配置好各项参数。
  2. 新建Plugin类,在编译流程中加入自定义的Transform任务,并获取用户配置的自定义属性值。
  3. 创建自定义的Transform,继承HunterTransform类,并将bytecodeWeaver变量赋值为自定义的Weaver。
  4. 自定义的Weaver继承BaseWeaver类,实现isWeavableClass方法返回是否要对当前扫描到的class文件做处理,实现wrapClassWriter方法返回自定义的字节码处理ClassVisitor。
  5. 自定义的ClassVisitor中实现visitMethod方法来返回自定义的MethodVisitor(使代码结构更清晰,不是必要的)。
  6. 自定义的MethodVisitor继承AdviceAdapter并实现onMethodEnter和onMethodExit方法,写入自己需要的ASM代码。ASM代码可通过ASMifier插件得到。
  7. 将项目发布到开源仓库中(可选)。

开发过程中,可能要进程Debug调试我们写的代码,gradle编译插件的调试方法,可以参考我这篇

Gradle进程调试方法https://blog.csdn.net/xingchenxuanfeng/article/details/89814011


参考:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值