自定义 Gradle Plugin

Plugin 的写法

最基本写法

       写在 module#build.gradle 里,如下所示:

1
2
3
4
5
6
7
8
//Plugin 的最基本写法
class PluginDemo implements Plugin<Project> {
@Override
void apply(Project target) {
println "Hello!"
}
}
apply plugin: PluginDemo

       plugin 是可以进行配置的,在下面的 module#build.gradle 中:

1
2
3
4
5
6
7
8
9
apply plugin: 'com.android.application'

android {
...
buildTypes {
...
}
...
}

       android 和 android 内部的一系列标签都是对 com.android.application 的配置。有的 plugin 不需要写配置也可以运行。如果需要动态配置,事实上可以写如下代码:

1
2
3
4
5
6
7
8
class PluginDemo implements Plugin<Project> {
@Override
void apply(Project target) {
def author = 'wy521angel'
println "Hello ${author}!"
}
}
apply plugin: PluginDemo

       但是和 com.android.application 的配置一样,我们需要变成类似下面的这种形式:

1
2
3
xxx {
name 'wy521angel'
}

Extension

       如下代码所示,这样写就和 com.android.application 的配置样式一样了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Plugin Extension
class PluginDemo implements Plugin<Project> {
@Override
void apply(Project target) {
def extension = target.extensions.create('wy', ExtensionDemo)
target.afterEvaluate {//加 afterEvaluate 表示延后执行
println "Hello ${extension.name}!"
}
}
}
class ExtensionDemo {
def name = 'Author'
}
apply plugin: PluginDemo
wy {
name 'wy521angel'
}

       由于代码是顺序执行的,当执行到 apply plugin: PluginDemo 时,void apply 内的方法会执行,如果不加 target.afterEvaluate 做延后执行,则打印出的是默认的“Author”。

       如果只是按照上面的写法写 plugin,事实上是没有意义的,plugin 是为了重用。

写在 buildSrc 目录下

buildSrc 目录下的写法

       目录结构如下图所示:

buildSrc目录结构

       resources/META-INF/gradle-plugins/*.properties 中的 * 是插件的名称,例如
*.properties 是 com.wy521angel.plugindemo.properties,最终在应用插件时代码如下所示:

1
apply plugin: 'com.wy521angel.plugindemo'

       *.properties 中只有一行,格式是:

1
implementation-class=com.example.plugindemo.PluginDemo

       其中等号右边指定了 Plugin 具体是哪个类。Plugin 和 Extension 写法和在 build.gradle 里的写法一样。

关于 buildSrc 目录

  • 这是 gradle 的一个特殊目录,这个目录的 build.gradle 会自动被执行,即使不配置进 settings.gradle;
  • buildSrc 的执行早于任何一个 project,也早于 settings.gradle。它是一个独立的存在;
  • buildSrc 所配置出来的 Plugin 会被自动添加到编译过程中的每一个 project 的 classpath,因此它们才可以直接使用 apply plugin: ‘xxx’的⽅方式来便捷应用这些 plugin;
  • settings.gradle 中如果配置了’:buildSrc’,buildSrc 目录就会被当做是子 Project,因此会被执行两遍。所以在 settings.gradle 里面应该删掉 ‘:buildSrc’ 的配置。

Groovy 两个语法点

  • getter / setter

       每个 field,Groovy 会自动创建它的 getter 和 setter 方法,从外部可以直接调用,并且在使用 object.fieldA 来获取值或者使用 object.fieldA = newValue 来赋值的时候,实际上会
自动转而调用 object.getFieldA() 和 object.setFieldA(newValue)

       这一点在 java 中是做不到的,groovy 和 java 可以相互调用,例如我们创建一个 JavaExtensionDemo,并在 PluginDemo 中使用

1
def extension = target.extensions.create('wy', JavaExtensionDemo)

       运行会报错,提示“Could not find method name()”,因为 Java 并不会帮我们创建 getter 和 setter 方法。

       Groovy 中下面有关 name 的三行是等效的:

1
2
3
4
5
wy{
name 'GEM'
setName('GEM')
name = 'GEM'
}
  • 字符中的单双引号

       单引号是不带转义的,而双引号内的内容可以使用 “string1${var}string2”的方式来转义。

Transform

       Transform 是由 Android 提供了,在项目构建过程中把编译后的文件(jar 文件和 class 文件)添加自定义的中间处理过程的工具。

       具体写法如下:

  • 先加上依赖:
1
2
3
4
5
6
7
8
// 因为 buildSrc 早于任何一个 project 执行,因此需要自己添加仓库
repositories {
google()
jcenter()
}
dependencies {
implementation 'com.android.tools.build:gradle:3.5.3'
}
  • 然后继承 com.android.build.api.transform.Transform ,创建一个子类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class TransFormDemo extends Transform {

// 构造方法
TransFormDemo() {
}

// 对应的 task 名
@Override
String getName() {
return "wyTransform"
}

// 要对哪些类型的结果进行转换(是字节码还是资源⽂文件)
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}

// 适用范包括什么(整个 project 还是别的),
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}

@Override
boolean isIncremental() {
return false
}

// 具体的“转换”过程
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
def inputs = transformInvocation.inputs
def outputProvider = transformInvocation.outputProvider

inputs.each {
// jarInputs:各个依赖所编译成的 jar 文件
it.jarInputs.each {
println "file:${it.file}"
File dest = outputProvider.getContentLocation(it.name, it.contentTypes, it.scopes, Format.JAR)
FileUtils.copyFile(it.file, dest)
}

// derectoryInputs:本地 project 编译成的多个 class 文件存放的目录
it.directoryInputs.each {
println "file:${it.file}"
File dest = outputProvider.getContentLocation(it.name, it.contentTypes, it.scopes, Format.DIRECTORY)
println "Dest: ${it.file}"
FileUtils.copyDirectory(it.file, dest)
}
}
}
}

       getInputTypes、getScopes 两个方法是需要配合使用的,例子中表示修改整个项目的字节码文件。transform 中的代码只是把编译完的内容原封不动搬运到目标位置,没有实际用处。要修改字节码,需要引入其它工具,例如 javassist。

  • 注册 transform

       代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void apply(Project target) {
def extension = target.extensions.create('wy2', ExtensionDemo)
target.afterEvaluate {
println "Hello ${extension.name}!"
}
//需要插入代码的 transform
def transform = new TransFormDemo()
//这是约定俗成的
//配置的类是 BaseExtension
//在 module 中的 build.gradle 里配置的标签是 android
//下面代码就是拿到对应配置类
def baseExtension = target.extensions.getByType(BaseExtension)
baseExtension.registerTransform(transform)
}

       bulid.gradle 里面的配制如下,代码必须要移到 android 标签之后,因为 PluginDemo2 增加了获取 android 标签配置类的功能。

1
2
3
4
apply plugin: 'com.wy521angel.plugindemo2'
wy2 {
name 'GEM2'
}

参考资料:
腾讯课堂 HenCoder

Fork me on GitHub