最近终于有机会做一些关于Android plugin相关的东西,之前虽然有学习过《Android 权威指南》一书,但是并没有进行一个实战操作,都是一些理论相关的学习。最近做了一个plugin主要是为了提取class文件里面的注解信息然后讲起搜集并上传。在实践中回过头发现很多知识都已经遗忘,所以本文对相关一些比较核心的知识进行一个回顾与梳理。

一、Gradle概述

Gradle的思维导图

在Android开发中,Gradle是每个开发者都会接触的,Gradle 是一个非常优秀的项目构建工具。这是大家都知道的,但是又有啥用呢?

最开始的时候很难理解gradle到底是干什么的,相关知识都比较离散,所以很多东西没有串起来,从而导致理解起来比较困难。

我自己总结就是:Gradle是一个构建工具,它存在的目的是产生一套“流水线”,对于安卓开发而言这个流水线就是从本地的编写代码以及资源整合到最终生成的产品过程。

用一个很形象的例子举例,我们现在要生产一包奶酪夹心饼干,于是我们得定义一个生产顺序:先让有的地方去生成饼干,有的地方生成出来奶酪,之后再让两块饼干夹着一块奶酪,最后再将它们装进一个小包装袋里面。

另一种情况:如果我想在奶酪中加一点果酱,那么我们不需要重新建立一套生产线,只需要在两块饼干与奶酪结合的过程中修改一下加入果酱的流程。

再另一种情况:如果我生产出来的奶酪夹心饼干不需要包装,那只需要在最后一个步骤让它另外走一条线路,毕竟没有包装的又不是不能吃,对吧?
奶酪饼干不同的生产流程
如上图所示,我们定义了三种流程,每种流程最后的产出物是不一样的,因为流程的“初始化”的东西是不一样的以及过程中的“配置”,所以“执行”的时候就不一样。

对比我们安卓开发:本地的Java文件以及资源文件就是对应的饼干以及奶酪,最终生成的面向用户的apk文件就是包装好的奶酪夹心饼干。

如果我们想打Debug包,那么就像是一个散装的饼干,我们能自己用用,但是还不能面向用户,如果想打Release包那么就是最终的产品形态能直接面向用户。

上面的例子讲得比较长,其实主要想让更多人能够更好地去理解gradle的用处。

当我们每次点击Android Studio的 run运行按钮之后,会看到控制台输出一大堆相关日志,例如下图所示:
Android Studio系统封装好的gradle Task
其实这些都是系统为我们封装好的一些task
点击 run 按钮,就相当于执行了一次 Gradle Task,一般来说,是Task assembleDebug或者Task assembleRelease

Gradle是目前Android主流的构建工具,无论通过命令行还是通过AndroidStudio来build,最终都是通过Gradle来实现的。以及Android领域的探索已经越来越深,不少技术领域如插件化、热修复、构建系统等都对Gradle有相关的需要。

二、Groovy

知道了Gradle的用处之后,我们很形象的知道Gradle是为了去产生一个流水线。那这个流水线是利用什么做到的呢?对于奶酪饼干生产的工厂他们是不同的车间机械工具直接的逻辑组装。而对于Gradle则是利用groovy语言编写出来的相关脚本从而来进行一个编译相关的配置。这里不再具体描述groovy语言的具体用法,这里我列举出来几个自己认为比较重要的几个技术点。

1、Closure(闭包)
闭包是的groovy语言具有,而Java语言不具有的特性,有人说Lambda表达式就是闭包,但是两则还是有一定的差异的,有兴趣的同学可以去看看这篇Java中Lambda表达式解析

定义闭的语意 :

{ [closureParameters -> ] statements }

其中[closureParameters->]代表参数,多参数用逗号分割,用->隔开参数与内容,没有参数可以不写->例如我们精彩在.gradle文件里面看到这样的内容:
闭包
其中projcet就是[closureParameters->]->之后的respositories就是statements,对于这段代码而言,statements里面又是一个闭包,如果改写成Java的样子就更形象了:

1
2
3
void subprojercts(Project projct) {
doSomething....
}

2、方法的输入参数优化
groovy中定义的函数,如果至少有一个参数,在调用的时候可以省略括号。比如这样

1
2
3
def func(String a){
println(a)
}
1
func 'hello'

在gradle有大量省略括号调用函数的例子,比如

1
2
3
4
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.targetSdkVersion
}

比如这里minSdkVersion 和 targetSdkVersion 其实就是调用了两个函数,传入了不同的参数,在AndroidStudio里面可以点进去查看函数实现

当然如果某个函数没有参数,那就不能省略括号,否则会当成一个变量使用

3、类的Property

如果类的成员变量没有加任何权限访问,则称为Property, 否则是Field,filed和Java中的成员变量相同,但是Property的话,它是一个private field和getter setter的集合,也就是说groovy会自动生成getter setter方法,因此在类外面的代码,都是会透明的调用getter和setter方法。

4、Trait

特性使用关键字 trait 声明,可以拥有普通成员和抽象成员。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

trait MessageHandler {
// 属性
int minLenght
// 方法
// 普通方法
void echo(String msg) {
println(msg)
}
// 抽象方法
abstract void show(String msg)
}
trait AnotherMessageHandler {
// 抽象方法
abstract void show(String msg)
}

class Message implements AnotherMessageHandler, MessageHandler {
.......
}

Groovy 中特质本质上是运行时对接口的实现,所以其方法的访问控制符只支持 public 和 private。从代码的书写可以看出来trait又像java中的abstract类又像interface
说他像interface是因为从编写上看就是使用了implements关键字,但是接口又不能使用普通方法。说他像抽象类,因为其内部使用了abstract定义抽象方法。但是它又能implements多个,而达到“多继承”的特性。因此它不是接口,也不是抽象类,它是 trait

三、Gradle的依赖

我们继续回到上面奶酪夹心饼干的生产上面,在产出奶酪夹心饼干之前,我们需要分别生产好单独的饼干与奶酪。假如我们的饼干原料有很多种,姑且我们叫他饼干v1,饼干v2……饼干vn ,奶酪也有很多种,我们叫它奶酪v1,奶酪v2……奶酪vn。那这么多种具体生产起来就应该有相关的选择,在Android开发中各种库都被单独抽了出来,只需要单独声明出来需要用哪个库即可。

我们平时看的的dependencies如下所示

1
2
3
4
5
6
7
8
9
10
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.victor:lib:1.0.4'
api 'com.android.support:recyclerview-v7:28.0.0'
implementation 'com.google.code.gson:gson:2.8.6'
implementation('com.wanjian:sak:0.1.0') {
exclude group: 'com.android.support', module: 'appcompat-v7'
}
}

1、依赖配置
Gradle依赖的配置中主要使用以下关键字,摘自谷歌官方Gradle文档->添加编译依赖项

implementation
Gradle 会将依赖项添加到编译类路径,并将依赖项打包到编译输出。不过,当您的模块配置 implementation 依赖项时,会让 Gradle 了解您不希望该模块在编译时将该依赖项泄露给其他模块。也就是说,其他模块只有在运行时才能使用该依赖项。

api
Gradle 会将依赖项添加到编译类路径和编译输出。当一个模块包含 api 依赖项时,会让 Gradle 了解该模块要以传递方式将该依赖项导出到其他模块,以便这些模块在运行时和编译时都可以使用该依赖项

annotationProcessor
要添加对作为注解处理器的库的依赖关系,您必须使用 annotationProcessor 配置将其添加到注解处理器类路径。这是因为,使用此配置可以将编译类路径与注解处理器类路径分开,从而提高编译性能。如果 Gradle 在编译类路径上找到注解处理器,则会禁用避免编译功能,这样会对编译时间产生负面影响(Gradle 5.0 及更高版本会忽略在编译类路径上找到的注解处理器)。

2、依赖的传递与冲突
在Maven仓库中,构件通过POM(一种XML文件)来描述相关信息以及传递性依赖。Gradle 可以通过分析该文件获取获取所以依赖以及依赖的依赖和依赖的依赖的依赖,为了更加直观的表述,可以通过下面的输出结果了解。

1
2
3
4
5
6
7
8
9
10
11
12
+--- com.github.hotchemi:permissionsdispatcher:2.2.0
| \--- com.android.support:support-v4:23.1.1 -> 28.0.0
| +--- com.android.support:support-compat:28.0.0
| | +--- com.android.support:support-annotations:28.0.0
| | +--- com.android.support:collections:28.0.0
| | | \--- com.android.support:support-annotations:28.0.0
| | +--- android.arch.lifecycle:runtime:1.1.1
| | | +--- android.arch.lifecycle:common:1.1.1
| | | | \--- com.android.support:support-annotations:26.1.0 -> 28.0.0
| | | +--- android.arch.core:common:1.1.1
| | | | \--- com.android.support:support-annotations:26.1.0 -> 28.0.0

我依赖hotchemi:permissionsdispatcher这个库,而它内部又陆陆续续地依赖了后面的一大堆。借助Gradle的传递性依赖特性,你无需再你的脚本中把这些依赖都声明一遍,你只需要简单的一行,Gradle便会帮你将传递性依赖一起下载下来。

然而问题来了这里面依赖了:android.arch.core:common:1.1.1 而我本地其他地方又使用了android.arch.core:common:1.0.0老版本。那我该如何去做这件事呢?

于是便有了如下关键词:
exclude
force
transitive

具体作用如下代码所示

1
2
3
4
5
6
7
8
9
implementation ('com.google.code.gson:gson:2.8.6') {
force = true //强制使用这个版本的库
}
implementation ('de.hdodenhof:circleimageview:3.0.1') {
transitive = true //防止向外暴露
}
implementation('com.wanjian:sak:0.1.0') {
exclude group: 'com.android.support', module: 'appcompat-v7' //排除里面不需要的库
}

四、Gradle的Task构建与执行

再回到生产饼干的例子上面来,最开始我们定义了一些流程,然后再让机器以该流程去执行。

比如先准备两块饼干再与奶酪进行加工生成夹心饼干,最后再加入包装。这是一条正确的流水,我们不可能让加入包装在加工生成夹心饼干之前。于是我们得定义一些约束,让其有正确的执行顺序。或者我们像加入果酱,那么就应该对原始的流程进行一些添加。

对于Gradle中,我们以Task为单位,类比生产奶酪饼干,生成饼干是一个专门的Task,生成奶酪也是一个专门的Task,加工成夹心也是一个Task……对于Android开发,将java文件编译为class,再到最后的dex生成都是Task

在Task的构建与执行中主要分为三个流程:

初始化(Initialization)
settings.gradle确定参与构建的module
为每个module创建Project对象实例

配置(Configuration )
build.gradle脚本执行,配置对应project实例
创建有向无环图
通过finalizedBy指定后续
通过must/shouldRunAfter约束执行顺序

执行(Execution )
根据关系图执行task
监听器

主要流程如图所示(图片摘自https://www.jianshu.com/p/0acdb31eef2d):
图片来自https://www.jianshu.com/p/0acdb31eef2d

五、Gradle插件

继续奶酪夹心饼干的故事,如果夹心饼干模样规规矩矩没有花纹,岂不是很low?于是工厂专门研发了一款能让饼干产生纹路的机器,并在加工成夹心饼干之前将纹路印到饼干上面去,假如这台机器我们把它叫做“印花纹机”,是一个能从整个生产流程中独立的出来的机器,这台“印花纹机”也能用在生产其他的饼干上。

对应在我们的Android开发中,在构建流程中我们抽离出来一些功能,将其独立开来,这就是plugin,这里不再讲解plugin的编写相关操作,可以参考Gradle 自定义 plugin

1、插件分类

脚本插件
顾名思义,如下图所示我们将对应的插件脚本中加入相关插件的逻辑,如下图所示,“other.gradle”便是一个插件

1
apply from: 'other.gradle'

二进制插件
二进制插件就是实现了 org.gradle.api.Plugin 接口的插件,每个 Java Gradle 插件都有一个 plugin id,可以通过如下方式使用一个 Java 插件:

1
apply plugin : 'maven'

通过上述代码就将 Java 插件应用到我们的项目中了,其中 maven 是 Java 插件的 plugin id,对于 Gradle 自带的核心插件都有唯一的 plugin id

2、打包方式

build script
在插件分类中我们提到有apply from: 'other.gradle' 其中other.gradle就是一个打包好的build script

buildSrc
将插件写在工程根目录下的buildSrc目录下,这样可以在多个模块之间复用该插件。
buildSrc是Gradle在项目中配置自定义插件的默认目录,但它并不是标准的Android工程目录,所以使用这种方式需要我们事先手动创建一个buildSrc目录
buildSrc插件
独立项目
创建独立的插件项目具有更强的灵活性,能让更多的工程使用这个插件,但流程也会相对复杂一点.这里不再具体讲解,可以参考Gradle 自定义 plugin

参考资料:

https://www.jianshu.com/p/6dc2074480b8
https://www.jianshu.com/p/bcaf9a269d96
https://juejin.im/entry/59918304518825489151732d
https://www.jianshu.com/p/0acdb31eef2d
https://juejin.im/post/5cc5929bf265da036706b350
https://doc.yonyoucloud.com/doc/wiki/project/GradleUserGuide-Wiki/gradle_plugins/binary_plugins.html