在上一篇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
,重写了两个方法。
isWeavableClass
方法,表示是否需要对该类进行处理。该transform任务会遍历所有的class,在遍历每一个class的时候都会调用该方法来判断是否要对该类进行处理。该方法的参数是类的全类名,我们这里判断了类名是否在配置文件里存在,存在的话就处理。wrapClassWriter
方法返回具体处理类字节码的ClassVisitor,这里返回了一个我们自定义的AddTryCatchClassAdapter
类。
以上,我们已经完成了基本框架的编写,下面就是具体处理类字节码了。
四 编写自定义ClassVisitor
Hunter框架会自动帮我们处理class字节码的输入输出,我们只要在wrapClassWriter
方法返回的自定义ClassVisitor类中处理字节码即可。
ClassVisitor是ASM框架提供的一个抽象类,我们可以通过实现该类来便捷的修改字节码,而无需关心字节偏移、class 文件的校验码等相对复杂的过程。
在ASM中,class文件被描述成树的结构,扫描class文件的过程中,会有顺序的调用visit
、visitAnnotation
、visitField
、visitMethod
、visitEnd
等方法。重写这几个方法,即可做到对字节码进行读取、修改。
有关ASM的知识,下面这篇文章写得很好,有兴趣的话可以看一下。
AOP 的利器:ASM 3.0 介绍
ASMifier
ASM的api学习起来有一定难度,我也不是很熟悉,但是这并不妨碍我们的开发。因为我们有ASMifier
插件!在Android Studio插件中搜索ASM Bytecode Outline 2017下载即可。
这个工具的作用是,显示生成对应的字节码所需的ASM代码,这对于不太熟悉ASM api的人来说帮助很大。
找到Try Catch对应的ASM代码
如下是我的使用方法:
- 先在设置里找到ASM Bytecode Outline的设置项,然后把Skip debug和Skip frames两项勾选上,这样可以减少部分不必要的ASM代码。
- 我们先写一个不带try catch的方法,然后在类上点击右键,在弹出菜单里点击
Show Bytecode outline
开始生成对应的字节码,再点击弹出框上的ASMified
按钮,即可显示生成我们当前类所需的ASM代码。 - 然后我们在该方法里加上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
方便的封装,提供了onMethodEnter
和onMethodExit
两个方法,分别表示扫描器进入方法和离开方法的时机。
我们自定义的AddTryCatchAdviceAdapter
继承AdviceAdapter
,并重写onMethodEnter
和onMethodExit
两个方法来在该方法的第一行和最后一行插入我们需要的字节码。把我们前面用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上。
步骤非常简单:
- 在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'
-
在GitHub上,点击releases ——> Draft a new release来创建一个新的release版本。
-
到 https://jitpack.io/ 上,使用自己的GitHub账号登录,选择自己的仓库后,点击Look up按钮,再对应版本后面点击Get it按钮即可。等待后台构建完成,就发布成功了。
总结
以上,就完成了AddTryCatch插件的编写。
我们再总结一下所有的步骤:
- 建立buildSrc moudle,并在其中的build.gradle文件中配置好各项参数。
- 新建Plugin类,在编译流程中加入自定义的Transform任务,并获取用户配置的自定义属性值。
- 创建自定义的Transform,继承HunterTransform类,并将bytecodeWeaver变量赋值为自定义的Weaver。
- 自定义的Weaver继承BaseWeaver类,实现isWeavableClass方法返回是否要对当前扫描到的class文件做处理,实现wrapClassWriter方法返回自定义的字节码处理ClassVisitor。
- 自定义的ClassVisitor中实现visitMethod方法来返回自定义的MethodVisitor(使代码结构更清晰,不是必要的)。
- 自定义的MethodVisitor继承AdviceAdapter并实现onMethodEnter和onMethodExit方法,写入自己需要的ASM代码。ASM代码可通过ASMifier插件得到。
- 将项目发布到开源仓库中(可选)。
开发过程中,可能要进程Debug调试我们写的代码,gradle编译插件的调试方法,可以参考我这篇
Gradle进程调试方法https://blog.csdn.net/xingchenxuanfeng/article/details/89814011