Gradle是一款专注于灵活性和性能的开源构建自动化工具。 Gradle构建脚本使用 Groovy 或 Kotlin DSL 编写,具有以下特点:

  • 高度可定制 — Gradle以最基本的方式进行定制和扩展
  • 快速 — Gradle通过重复使用先前执行的输出,仅处理已更改的输入以及并行执行任务来快速完成任务
  • 强大 — Gradle是Android的官方构建工具,并且支持许多流行的语言和技术

    安装

    一行命令搞定法:
1
2
3
$ brew install gradle
# 验证是否安装成功
$ gradle -v

基础

Projects & tasks

任何一个 Gradle 构建都是由一个或多个 projects 组成,每个 task 都代表了构建执行过程中的一个原子性操作。如编译,打包,生成 javadoc,发布到某个仓库等操作

Hello World

1
2
3
4
5
6
7
task hello {
doLast {
println 'hello world'
}
}

// Run:gradle -q hello
  1. 首先定义了一个叫做 hello 的 task
  2. doLast - Adds the given closure to the end of this task’s action list,可以从官方文档中看的出来,doLast 把参数中的闭包放到了所有 task 的最后执行
  3. 其中 -q 参数表示只打印错误信息,不会打印其他日志信息

需要注意的是下面 task 的写法:

1
2
3
task why {
println "hello"
}

这种写法属于给当前文件中的任务增加共同行为

快速定义

1
2
3
task append << {
println 'hello world'
}

<< 操作符不能理解为 Groovy 中的追加操作,必须理解为 doLast 的简单写法。需要的注意的是 doLast 默认会按顺序执行所在文件中所有的 task

任务依赖

任务依赖主要用 dependsOn ,有两种用法,第一种为常规用法:

1
2
3
4
5
6
7
8
9
10
11
12

task append << {
println 'append'
}

task depend(dependsOn: append) << {
println 'depend'
}

task compile(dependsOn: [append, denpend]) << {
println 'compile'
}

第二种是当被依赖的 task 声明在当前 task 之后的写法:

1
2
3
4
5
6
7
8
// 注意区别,这里 task 名字加上了单引号
task depend(dependsOn: 'append') << {
println 'depend'
}

task append {
println 'append'
}

动态创建任务

使用 Groovy 循环方式也可以创建 task:

1
2
3
4
5
1.upto(5){
task "task_$it" << {
println "$it"
}
}

使用已存在的 task

1
2
3
4
5
6
7
8
9
10
11
12
13
task_1.dependsOn task_2, task_3

task_1.doFirst {
println "do $task_1.name"
}

task_1.doLast {
println "finish $task_1.name"
}

task_1 << {
println "finish $task_1.name again"
}

task 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
// 其中 $task_1.name 就是默认的 task 属性
task_1 << {
println "finish $task_1.name again"
}

// 自定义属性
task config {
ext.customName = 'kevin'
}

task test << {
println config.customName
}

默认 task

使用 defaultTasks 定义默认 task,多个 task 使用逗号区分:

1
defaultTasks 'append', 'hello'

为不同 task 配置不同参数

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
File[] getFiles(dir) {
file(dir).listFiles({file -> file.isFile()} as FileFilter).sort()
}

// 获取文件内容
task loadDirFile << {
def files = getFiles("$dir")
if (files.length > 0) {
files.each { file ->
ant.loadfile(srcFile: file, property: file.name)
println "${ant.properties[file.name]}"
}
}
}

// 生成校验值
task checkSum << {
def files = getFiles("$dir")
if (files.length > 0) {
files.each { file ->
ant.checksum(file: file, property: file.name)
println "${ant.properties[file.name]}"
}
}
}

gradle.taskGraph.whenReady { task ->
if (task.hasTask(checkSum)) {
ext.dir = ".."
} else if (task.hasTask(loadDirFile)) {
ext.dir = "."
}
}

Gradle Wrapper

运行 Gradle Script 的推荐方式就是使用 Gradle Wrapper,先看一下 Gradle Wrapper 的工作流程:

Wrapper是一个调用Gradle声明版本的脚本,如果需要的话可以事先下载。因此,开发人员可以快速启动并运行Gradle项目,不用担心本地 Gradle 版本和项目中使用的 Gradle 版本不一致导致一些不可预见的问题,从而节省公司的时间和金钱。

Gradle 命令行

执行 gradle -h 可以查看所有参数以及描述

多任务调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
task compile << {
println 'compile'
}

task compileTest(dependsOn: compile) << {
println 'compileTest'
}

task runTest(dependsOn: [compile, compileTest]) << {
println 'runTest'
}

task running(dependsOn: runTest) << {
println 'running'
}

执行 gradle running compile 后的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
➜ gradle running compile 

> Task :compile
compile

> Task :compileTest
compileTest

> Task :runTest
runTest

> Task :running
running

这是因为每个 task 只会执行一次,running 已经依赖了 compile,所以 compile 不会执行两次

排除任务

-x, –exclude-task Specify a task to be excluded from execution.
指定一个 task 不执行

1
2
3
4
5
6
7
8
9
10
➜ gradle running -x compile

> Task :compileTest
compileTest

> Task :runTest
runTest

> Task :running
running

失败后继续执行

–continue Continue task execution after a task failure.
当一个 task 失败后继续往后执行

简化任务名

需要执行任务时,无须输入全部任务名,只需提供足够的可以唯一区分出该任务的字符即可

指定构建文件

-b, –build-file Specify the build file.
指定一个构建文件

获取任务信息

tasks - Displays the tasks runnable from root project ‘gradle’
获取任务新

1
2
3
4
// 获取所有的 task
$ gradle -q tasks --all
// 获取 task 的详细信息
$ gradle help --task taskName

配置任务参数

1
2
3
4
5
task hello {
description = "hello"
version = "10.0.0"
group = "build"
}

获取依赖信息

针对 Android 项目的一个例子:

1
2
3
4
5
6
// 获取所有 projects
$ gradle -q projects
// 选择其中一个或多个获取它的依赖信息
$ gradle -q app:dependencies lib:dependenices
// 获取 Gradle 构建脚本的依赖项
$ gradle buildEnvironment

获取指定依赖信息

首先需要明白 configuration 的概念:

What is a configuration?

Every dependency declared for a Gradle project applies to a specific scope. For > example some dependencies should be used for compiling source code whereas others only need to be available at runtime. Gradle represents the scope of a dependency with the help of a Configuration. Every configuration can be identified by a unique name.

Many Gradle plugins add pre-defined configurations to your project. The Java plugin, for example, adds configurations to represent the various classpaths it needs for source code compilation, executing tests and the like. See the Java plugin chapter for an example. The sections above demonstrate how to declare dependencies for different use cases.

上面是官方的解释,简单翻译后可以理解为:

每个 Gradle 项目的依赖都有一个特定的范围,比如 implementation 以及 api,根据这些配置,Gradle 才能知道该怎么处理这些依赖。其实 implementation 以及 api 都是 Android 项目预定义的 configuration

1
2
// 获取指定 configuration 的依赖信息
$ gradle app:dependencies --configuration implementation

获取特定依赖报告

1
2
3
// --dependency 是具体依赖 --configuration 是依赖的具体配置,缺一不可
$ gradle -q app:dependencyInsight --dependency com.facebook.stetho
--configuration compileClasspath

dependencyInsight 可以查看依赖深度报告,用来排查依赖冲突非常有效

获取属性

1
$ gradle -q app:properties

构建日志

加上 --profile 参数就可以在根目录的 build/reports/profile 中生成一个以日期命名的日志文件:

1
$ gradle -q --profile  getConfigurationByName

Dry Run

只想知道某个任务在一个任务集中按顺序执行的结果,但并不想实际执行这些任务,可以使用 -m 参数:

1
$ gradle -m build

给 JVM 添加参数

1
$ gradle -Dmyprop=myprop

Gradle 的 -D 参数和 Java 的 -D 参数功能一致

Gradle DSL

DSL 的全称就是 Domain Specific Language(领域特定语言),通过例子可能会更好的理解:

  1. 正则表达式 - 按照指定规则编写字符串,是否匹配全交给了语言引擎来完成
  2. SQL - 可以看作数据库领域的 DSL
  3. Markdown - 写作领域的 DSL

声明变量

局部变量

只在定义该变量的文件中有效的变量:

1
2
3
4
5
6
// build.gradle
def value = 'kevin'

task getValue {
println value
}

额外属性

额外属性可以通过所属对象的 ext 属性进行添加,读取和设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// root/build.gradle
ext {
value = 'world'
}

ext.a = 'a'
ext.b = [1,2,3]
ext.c = [:]
c.a = 'c.a'
c.b = 'c.b'

// root/app/buidl.gradle
def value = 'kevin'

task getValue << {
println "Local variable is $value, Global variable is $c.a"
}

同时也可以使用 gradle.properties 文件为 project 对象添加额外参数,如果当前目录中不存在配置文件,读取项目根目录中的配置文件

-P 参数设置属性:

1
2
3
4
5
// command: gradle -q -PcommandValue=hello2 getCommandValue
// output: hello2
task getCommandValue << {
println commandValue
}

-D 参数设置属性:

1
2
3
4
5
// command: gradle -q -DsystemValue=hello getSystemValue
// output: hello
task getSystemValue << {
println System.getProperty("systemValue")
}

通过系统变量设置属性,当有类似 ORG_GRADLE_PROJECT_propName=value 这样格式的系统变量时,Gradle 里面就可以使用 propName 属性,这样就可以设置一个:

1
2
3
4
5
6
7
// command:
// export ORG_GRADLE_PROJECT_value=kevin
// gradle -q getSystemValue2
// output: kevin
task getSystemValue2 << {
println value
}

还有一种情况是需要设置系统变量时,就可以通过 gradle.properties 文件来设置,如果该文件中有一个 systemProp. 为前缀的属性,该属性和它对应的值就会被添加到系统属性中,且不带前缀。需要注意的是,在多项目构建中,除了根目录的 systemProp. 属性,其他目录下的都将被忽略:

1
2
3
4
5
6
7
8
// gradle.properties
systemProp.helloWorld=hello
// build.gradle
// command: gradle -q getSystemValue
// output: hello
task getSystemValue << {
println System.getProperty("helloWorld")
}

如果同时在 gradle.properties 和命令行中都设置了同一个参数,则优先使用命令行中配置的参数

闭包委托

修炼中…

使用外部构建脚本

1
2
3
4
5
6
7
8
9
10
11
// other.gradle
ext.otherValue='other'
ext.otherMethod={
println "other.gradle#otherMethod"
}
// build.gradle
apply from: 'other.gradle'
task getOtherScriptValue << {
otherMethod()
println otherValue
}

缓存

Gradle 默认缓存所有编译过的脚本,如果有脚本有改动重新编译并缓存,没有改动则使用缓存不进行编译。使用 --recompile-scripts 选项运行 Gradle 会强制丢弃缓存重新编译并缓存

Task

定义任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
task myTask{
println 'myTask'
}

task(myTask){
println 'myTask'
}

task('myTask'){
println 'myTask'
}

task(myTask, type: Copy){
from "."
into ".."
include '*.gradle'
}

tasks.create(name: 'createTask') {
println 'crate task'
}

重写任务

1
2
3
4
5
6
7
8
9
10
tasks.create(name: 'createTask') << {
println 'crate task'
}

task createTask(overwrite: true) << {
println 'overwrite'
}

// output:
// override

必须把 overwrite 属性设置为 true,否则会抛出 a task with that name already exists 的异常

自定义类型 Task

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class CustomTask extends DefaultTask {
String value = 'hello workd'

@TaskAction
def init() {
println "i'm custom task -- $value"
}
}

task customTask1(type: CustomTask)


task customTask2(type: CustomTask) {
value = 'custom prop'
}
// command: gradle -q customTask2 hello
// output: i'm custom task -- custom prop
// output: i'm custom task -- hello workd

该任务添加一个方法并使用 @TaskAction 注释标记它。当任务执行时,Gradle将调用该方法

跳过任务

onlyIf

Typical usage:myTask.onlyIf{ dependsOnTaskDidWork() },当闭包中的内容返回 true 时,才会执行该 Task:

1
2
3
4
5
6
7
8
createTask.onlyIf {
project.hasProperty('skip')
}

// command: gradle createTask
// output: BUILD SUCCESSFUL in 0s
// command: gradle -q createTask -Pskip
// output: overwrite

StopExecutionException

1
2
3
4
5
6
7
8
9
10
11
12
task compile << {
println 'We are doing the compile.'
}
compile.doFirst {
if (true) { throw new StopExecutionException() }
}
task myTask(dependsOn: 'compile') << {
println 'I am not affected'
}

// command: gradle -q myTask
// output: I am not affected

Task enable 属性

每个 task 都有一个值为 true 的 enable 属性,如果把这个属性设置为 false,则不会执行该 task 的任何操作

任务规则

1
2
3
4
5
6
7
8
9
10
tasks.addRule("Pattern: ping<ID>") { String taskName ->
if (taskName.startsWith("ping")) {
task(taskName) << {
println "Pinging: " + (taskName - 'ping')
}
}
}

// command: gradle -q pingHello
// output: Pinging: Hello

日志

等级

Level Feature
ERROR 错误消息
QUIET 重要的信息消息
WARNING 警告
LIFECYCLE 进度
INFO 信息性消息
DEBUG 调试消息

日志命令参数

选项 输出
LIFECYCLE 及更高
–quiet QUIET 及更高
–info INFO 及更高
–debug DEBUG 及更高

堆栈跟踪参数

选项 含义
构建错误(如编译错误)时没有栈跟踪打印到控制台。只有在内部异常的情况下才打印栈跟踪。如果选择 DEBUG 日志级别,则总是输出截取后的栈跟踪信息
–stacktrace 输出截断的栈跟踪。我们推荐使用这一个选项而不是打印全栈的跟踪信息。Groovy 的全栈跟踪非常冗长 (由于其潜在的动态调用机制,然而他们通常不包含你的的代码中哪里错了的相关信息)
–full-stacktrace 打印全栈的跟踪信息

打印日志

1
2
3
4
5
6
7
8
prinln 'log'
logger.quiet('An info log message which is always logged.')
logger.error('An error log message.')
logger.warn('A warning log message.')
logger.lifecycle('A lifecycle info log message.')
logger.info('An info log message.')
logger.debug('A debug log message.')
logger.trace('A trace log message.')

自定义 configuration

1
2
3
4
5
6
7
8
9
10
11
12
13

configurations {
custom
}

dependencies {
custom 'com.facebook.stetho:stetho:1.5.0'
}

task copyDependencies(type: Copy){
from configurations.custom
into 'build/bins'
}

可以根据自定义的 configurations 做一些不一样的事情,比如把自定义 configurations 所依赖的库拷贝到指定的文件夹中

debug buildSrc

只用改一个你想要的名字即可,其他参数不用变:

下好断点位置后,执行如下指令:

1
$ ./gradlew assembleDebug -Dorg.gradle.daemon=false -Dorg.gradle.debug=true

看到这个提示后再执行 debug 操作:

自定义插件

参考资料
参考资料
官方 dsl demo
kts 配置 uploadArchives
中文 demo

小技巧

设置 apk 文件名称

1
2
3
4
5
android.applicationVariants.all { variant ->
variant.outputs.all {
outputFileName = "${defaultConfig.applicationId}-${variant.productFlavors[0].name}.apk"
}
}

util.gradle

总结了常用的一些工具函数 util.gradle

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
ext.getCurrentFlavor = {
Gradle gradle = getGradle()
String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()

Pattern pattern

if (tskReqStr.contains("assemble"))
pattern = Pattern.compile("assemble(\\w+)(Release|Debug)")
else
pattern = Pattern.compile("generate(\\w+)(Release|Debug)")

Matcher matcher = pattern.matcher(tskReqStr)

if (matcher.find()) {
println matcher.group(1).toLowerCase()
return matcher.group(1).toLowerCase()
} else {
println "NO MATCH FOUND"
return ""
}
}

ext.getCurrentBuildType = {
Gradle gradle = getGradle()
String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()

if (tskReqStr.contains("Release")) {
println "getCurrentBuildType release"
return "release"
} else if (tskReqStr.contains("generateDebug")) {
println "getCurrentBuildType debug"
return "debug"
}

println "NO MATCH FOUND"
return ""
}

build.gradle 中使用:

1
2
3
4
apply from: 'util.gradle'
task callOtherScriptMethod {
getCurrentBuildType()
}

实例

讲了这么多,下面列举几个常见的 Gradle 配置:

加速Gradle构建

阿里国内镜像,下载速度很快,从此告别添加依赖后再等上十分钟

1
2
3
4
5
allprojects {
repositories {
maven{ url 'http://maven.aliyun.com/nexus/content/groups/public/'}
}
}

修改默认 apk 文件名

很多时候需要通过 apk 的命名很直观的看到一些信息,比如版本号,渠道以及是否测试版等等,此时就需要修改 apk 文件名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
buildTypes {
...

applicationVariants.all { variant ->
variant.outputs.each { output ->
def outputFile = output.outputFile
if (outputFile != null && outputFile.name.endsWith('.apk')) {
// 输出apk名称为LoveDev-v1.0.apk
def fileName = "LoveDev-v${defaultConfig.versionName}.apk"
output.outputFile = new File(outputFile.parent, fileName)
}
}
}
...
}

自动生成 versionCode 和 versionName

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
// versionCode  : git commit 次数 ;
// versionName : 大版本号.小版本号.修改版本号.编译版本号 ;
// 获取 version code
static def getVersionCode(boolean isDebug) {
if (isDebug) {
return Integer.parseInt(new Date().format("yyMMddHHmm"))
}
return getRevisionNumber()
}

// 获取 version name
def getVersionName(boolean isDebug) {
String version = rootProject.ext.appmajor +
'.' + rootProject.ext.appminor +
'.' + getRevisionNumber()
String today = new Date().format('yyMMdd')
String time = new Date().format('HHmmss')
if (isDebug) {
return version + ".$today.$time." + getRevisionDescription()
}
return version + ".$today." + getRevisionDescription()
}

// 获取修订版本 git 提交次数
static def getRevisionNumber() {
Process process = "git rev-list --count HEAD".execute()
process.waitFor()
return process.getText().toInteger()
}

// 获取修订版本最后一次 git 记录后6位
static def getRevisionDescription() {
String desc = 'git describe --always'.execute().getText().trim()
return (desc == null || desc.size() == 0) ? new Date().format("yyMMdd") : desc.substring(desc.size() - 6)
}

productFlavors 配置产品风味

如果除了正式版,测试版之外还要继续细分化,针对不同的渠道,正式版要分为收费版,免费版等情况,这时候就需要用到 productFlavors 来配置了

将配置信息添加到 productFlavors {} 代码块并配置想要的设置。产品风味支持与 defaultConfig 相同的属性,这是因为 defaultConfig 实际上属于 ProductFlavor 类,我在实际项目中碰到一个需求,针对某一个功能方法,分为两个版本处理,除此之外在 CI 环境中还要同时上传这两个版本,这就意味着两个版本的包名也要不同,下面是示例代码:

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
// buildConfig 中配置AUDIO_VERSION字段
productFlavors {
shine {
buildConfigField "int", "AUDIO_VERSION", "2"
versionCode 2
}
webrtc {
buildConfigField "int", "AUDIO_VERSION", "1"
versionCode 1
}
}

// buildTypes 针对不同的产品风味,构建不同的 apk 文件名
buildTypes {
...

applicationVariants.all { variant ->
variant.outputs.each { output ->
def outputFile = output.outputFile
if (outputFile != null && outputFile.name.endsWith('.apk')) {
// 这里添加productFlavors的名字,输出apk名称为LoveDev-v1.0-webrtc.apk
def fileName = "LoveDev-v${defaultConfig.versionName}-${variant.productFlavors[0].name}.apk"
output.outputFile = new File(outputFile.parent, fileName)
}
}
}
...
}

配置之后在 buildType 类中就有了int 类型的 AUDIO_VERSION 字段,可以根据该字段判断方法中的具体实现

其实很多配置,在官网里面已经提到过了,不过还是要根据具体的项目进行具体的配置,尽量不要照抄照搬,官网地址请戳这里

sourceSets 设置源集

如果需要为一个程序在逻辑不变的情况下配置多套界面,就需要用到 sourceSets 来配置项目源集,现在有如下结构的一个程序:

1
2
3
4
+--main
| +--java
| +--skin-custom
| +--res

其中 skin-custom 存放的就是定制皮肤,build.gradle 需要进行如下配置:

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

productFlavors {
standard {
dimension "skin"
}
skin {
dimension "skin"
}
}

sourceSets{
standard {
res.srcDirs = ['src/main/res']
}

skin {
res.srcDirs = ['src/main/skin-custom']
}
}

如果在 skin-custom 中获取不到需要的文件,就遍历其他文件夹寻找该文件

忽略 release buildType

1
2
3
4
5
6
7
8
9
10
android{
...
variantFilter { variant ->
def names = variant.buildType.name
if (names.contains("release")) {
setIgnore(true)
}
}
...
}