在软件开发领域,构建工具是开发过程中不可或缺的一部分。它们承担着将源代码转换为可执行程序或库的重要任务,并且在自动化构建、依赖管理和持续集成方面发挥着关键作用。在众多构建工具中,Gradle 以其强大的功能和灵活的特性成为了官方推荐的 Android 项目构建工具。
1. Gradle 简介
Gradle 是一个基于 Apache Ant 和 Apache Maven 概念的构建自动化工具,但它更加灵活和强大。与传统的 XML 配置相比,Gradle 使用 Groovy 或 Kotlin DSL(领域特定语言)编写构建脚本,使得构建逻辑更加直观和易于理解。Gradle 支持多项目构建、依赖管理、增量构建等功能,并且具有良好的扩展性,可以通过插件机制轻松扩展其功能。
2. Gradle 生命周期
Gradle 构建过程可以分为三个阶段:
- 初始化(Initialization)
- 配置(Configuration)
- 执行(Execution)
在初始化阶段,Gradle 准备执行环境;在配置阶段,Gradle 配置项目结构、任务和依赖关系;在执行阶段,Gradle 执行项目中定义的任务并完成构建过程。
2.1 Gradle 初始化阶段(Initialization)
Gradle 初始化阶段是构建过程的第一步,负责准备执行环境。在初始化阶段,Gradle 会执行用户目录下(C:\Users\%USERNAME%\.gradle\
)的 init.d\
文件夹中(init.d\
文件夹默认不存在,需要手动创建)的 gradle 文件。这个文件夹中可以放置多个 gradle 文件,这个阶段会按照字母序依次执行这些文件。
例如,可以在 init.d\
文件夹中创建一个 init.gradle
文件,写入以下内容:
scss
println("gradle version: ${getGradleVersion()}")
ext.timestamp = {
new Date()
}
getGradleVersion()
是 Gradle 接口中的函数,Gradle 完整方法参见: docs.gradle.org/current/jav...
在项目每次执行时,就会打印出当前的 gradle 版本,例如:gradle version: 8.2
,并且会创建一个 timestamp 的全局属性,在项目中通过 gradle.timestamp()
可以读取此属性。
go
// 输出 timestamp: Thu Apr 18 16:06:46 CST 2024
println("timestamp: ${gradle.timestamp()}")
初始化阶段可以用于实现一些全局性的配置和初始化操作。
在多项目构建时,(Multi-Project Build),初始化阶段还会解析 settings.gradle 中的内容,该文件用于定义项目的结构(包含的子项目),以及一些全局配置。
项目的 settings.gradle 示例代码:
php
// 指定包含的子项目
include 'subproject1', 'subproject2'
// 指定包含其他文件夹下的子项目
include ':subproject_in_other_folder'
// 指定其他文件夹下的子项目所在的文件夹
project(':subproject_in_other_folder').projectDir = file('<path_of_the_folder_contains_the_subproject>')
// 指定 rootProject 的 name
rootProject.name = "MyProject"
// 指定 rootProject 的构建文件名称,默认为 build.gradle
rootProject.buildFileName = "myCustomName.gradle"
include(), getRootProject() 是 Settings 接口中的函数,Settings 完整方法和属性参见: docs.gradle.org/current/jav...
2.2 Gradle 配置阶段(Configuration)
Gradle 配置阶段是构建过程的核心,负责定义和配置项目的各个任务。在配置阶段,Gradle 会按照构建脚本逐步解析和配置项目中的任务和依赖关系。通过 DSL(Domain-Specific Language,领域特定语言)提供的丰富功能,开发者可以灵活地定义任务、管理依赖和配置构建流程,为后续的执行阶段做好准备。
项目的 build.gradle 示例代码:
scss
// 添加依赖库仓库
repositories {
mavenCentral()
}
// 添加依赖库
dependencies {
implementation 'org.apache.commons:commons-math3:3.6.1'
}
repositories(), dependencies(), getTasks() 是 Project 接口中的函数,Project 完整方法和属性参见: docs.gradle.org/current/jav...
2.2.1 Gradle 中的代理模式
- 所有的 gradle 文件都实现了 Gradle 接口。在一般的 gradle 文件中,上下文是 Gradle 的代理对象,可以使用 Gradle 接口中的方法。
- build.gradle 额外实现了 Project 接口。在 build.gradle 文件中,上下文是 Project 的代理对象,可以使用 Project 接口中的方法。通过 project.gradle 可以拿到 Gradle 代理对象。
- settings.gradle 额外实现了 Settings 接口。在 settings.gradle 文件中,上下文是 Settings 的代理对象,可以使用 Settings 接口中的方法。通过 settings.gradle 可以拿到 Gradle 代理对象。
2.2.2 Gradle 中的属性
gradle 支持定义 key-value
属性,可以将其放到项目的 gradle.properties
文件中,或者 gradleUserHomeDir
中的 gradle.properties
文件中,也可以通过命令行 -Pkey=value
传入。
使用属性前,最好先用 hasProperty()
函数做个检查,更为安全。
2.2.3 Task 和 Action
gradle 的 configuration 阶段用于向 project 中定义、配置好各个 tasks。execution 阶段用于真正执行这些 tasks。一个 project 由 0 个或多个 tasks 构成,一个 task 由 0 个或多个 actions 构成。
task 中有两个非常方便的方法 doFirst 和 doLast,这两个方法可以用于给指定的 task 添加 action。
arduino
// 自定义 task
tasks.register('customTask') {
doFirst {
println 'Before customTask'
}
doLast {
println 'After customTask'
}
}
Task 的所有方法参见: docs.gradle.org/current/jav...
doFirst 有多个时,最后加的会最先执行。doLast 有多个时,最后加的会最后执行。换句话说,doFirst 添加的 action 是加到所有 action 最前端的,doLast 添加的 action 是加到所有 action 最后端的,恰如其名。
通过 dependsOn 可以设置 task 执行顺序:
arduino
// 修改 Task
clean.doFirst {
println 'Start to clean'
}
// 配置 Task 之间的依赖关系,下面这行代码意味着 clean task 需要等待 customTask 执行完成后才能执行。
clean.dependsOn(customTask)
执行 clean,输出如下:
css
Before customTask
After customTask
Start to clean
2.2.4 TaskGraph
在 Configuration 阶段定义好 Task,并设置好依赖关系后。在提交阶段前会生成一个 TaskGraph,也就是在提交阶段需要被执行的所有 Task 的拓扑图。通过 project.gradle.taskGraph.getAllTasks()
可以获取 TaskGraph 中的所有 Task,由于 TaskGraph 是在 Configuration 阶段完成后才能知道的,所以不能直接获取到,而是需要在 taskGraph 的 whenReady 方法中读取:
scss
project.gradle.taskGraph.whenReady {
it.getAllTasks().each {
println("Task: $it")
}
}
执行一次 clean,输出如下:
arduino
Task: task ':customTask'
Task: task ':clean'
TaskGraph 是一个有向无环图,通过 getDependencies()
可以获取某个 Task 的依赖关系:
scss
project.gradle.taskGraph.whenReady {
it.getDependencies(clean).each {
println("Task: $it")
}
}
执行一次 clean,输出如下:
arduino
Task: task ':customTask'
getAllTasks() 和 getDependencies() 都是 TaskExecutionGraph 接口中的方法,TaskExecutionGraph 的所有方法参见: docs.gradle.org/current/jav...
TaskExecutionGraph 还有一个常用的方法 hasTask()
,用于判断某个 task 是否在 graph 中。接收的参数可以是 task name,或 task 本身。
2.2.5 使用插件
插件中可以修改配置,添加 task,添加 action 等。相当于将配置阶段的某些操作进行封装,以便更好地复用。
以使用 java 插件为例,添加插件可以写成:
bash
plugins {
id 'java'
}
或者:
arduino
apply plugin: 'java'
在使用 java 插件后,项目中就引入了其中集成的 task,如生成 java 文档的 javadoc task、打包 jar 文件的 jar task。
可以修改这些 task 的配置,比如修改 jar 文件名称:
ini
jar {
baseName = "customName"
}
Apply java plugin 后,添加的 task 参见:docs.gradle.org/current/use...
也可以自定义插件,将项目中比较独立的、可以复用的 task 和配置进行封装。这个话题可以单独写一篇文章,这里推荐两篇写得不错的文章:
2.2.6 依赖管理和传递性依赖(Transitive Dependencies)
Gradle 提供了强大的依赖管理机制,支持直接依赖(Direct Dependencies)和传递性依赖(Transitive Dependencies)。Gradle 还支持排除特定的传递性依赖,以解决版本冲突和依赖冲突的问题。
引入依赖库仓库:
scss
repositories {
mavenCentral()
}
这里也用到了代理模式,在进入 repository block 之前,当前的 delegate object 是 project,可以调用 Project 接口中的方法。进入之后,当前的 delegate object 变成了 repository 提供的代理对象,相当于切换了 Context。可以直接调用 repository 中的方法,比如 mavenCentral()
。
所以引入依赖库仓库也可以写成:
markdown
project.repositories {
repositories.mavenCentral()
}
或者:
csharp
delegate.repositories {
delegate.mavenCentral()
}
mavenCentral()
引入的依赖库仓库地址是什么呢?查看 Project 接口文档中关于 repository {}
的介绍可以发现,它的上下文中,提供的代理对象是 RepositoryHandler。再查看 RepositoryHandler 的文档 就能找到 mavenCentral()
代表的实际 URL:repo.maven.apache.org/maven2/
MVN Reporitory 地址:mvnrepository.com/ ,可以在这个网站查询 mavenCentral() 中有哪些仓库,如何添加依赖。
同样地,可以找到 google() 代表的实际 URL:dl.google.com/dl/android/...
但貌似 google 没有提供一个网页来展示其所有库,如果有读者找到,望不吝赐教。
添加依赖库仓库只是添加依赖的第一步,通过 dependencies {}
才是真正添加依赖库。
通过同样的方式可以查到 dependencies 的代理对象是 DependencyHandler:docs.gradle.org/current/dsl... ,其中可以找到
dependencies {}
代码块中可以调用的函数。
注:implementation 函数是 Gradle Java Plugin 中的,api 函数是 Gradle Java Library Plugin 中的。
以 mvnrepository.com/artifact/or... 为例,引入依赖库的完整写法:
csharp
implementation group: 'org.apache.commons', name: 'commons-math3', version: '3.6.1'
简单写法:
arduino
implementation 'org.apache.commons:commons-math3:3.6.1'
Dependencies 按照引用方式,可以分为 Direct Dependencies 和 Transitive Dependencies。 Direct Dependencies 指的是直接在项目中声明的依赖,Transitive Dependencies 指的是 Direct Dependencies 中带过来的依赖。
查看 app 模块下的依赖:
gradlew app:dependencies
如果依赖项较多,不方便在控制台查看的话,可以将其输出到文件中:
gradlew app:dependencies >dependencies.txt
还可以利用 project-report 插件生成 report,添加插件:
arduino
apply plugin: 'project-report'
执行 task:
gradlew htmlDependencyReport
执行后,会在 app/build/reports/ 目录中生成一份 report,点击 app/build/reports/project/dependencies/index.html
可以查看 dependency report:
其中,implementation
中包含的是 Direct dependencies,RuntimeClassPath 中包含 Direct dependencies 和 Transitive dependencies。
Gradle project-report plugin doc:docs.gradle.org/current/use...
排除某个 Transitive Dependencies 示例代码:
javascript
implementation('commons-beanutils:commons-beanutils:1.9.4') {
exclude group: 'commons-collections', module: 'commons-collections'
}
2.2.7 Multi-Project Build
Multi-Project 由 Root Project 和多个依赖的 Sub Projects 组成,Sub Project 也可以继续依赖其他 Sub Project,Sub Project 之间也可以互相依赖。
在 Root project 执行 clean 或 build 时,所有的 Sub Projects 也会执行 clean 和 build。
Root project 需要有一个 settings.gradle 文件,在 Single project Build 时,这个文件是可选的,但 Multi-Project Build 必须有这个文件。这个文件用于标记此项目包含哪些子项目,也可以用于设置所有子项目中共有的属性。
在 rootProject 的 build.gradle 文件中,可以设置子项目共有的属性:
ini
subprojects {
apply plugin: 'java'
group = 'com.mygroup'
version = '0.1'
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
}
}
除了 subprojects 方法外,还有个 allProjects 方法,包含 Root Project 和 Sub Projects:
bash
allprojects {
println("$it")
println("$it.name")
println("$it.path")
}
也可以通过 rootProject.childProjects 来访问 Sub Projects:
bash
rootProject.childProjects.each {
println("$it")
println("$it.key, $it.value, $it.value.name")
}
通过 project(path)
可以获取单个 Project:
scss
project(':') {
println("$it")
println("$it.name")
}
project(':app') {
println("$it")
println("$it.name")
}
将某个 project 添加为依赖也是用这种类似的方式:
java
dependencies {
implementation project(':lib')
}
如果要将 transitive dependencies 也引用进来,需要用 api 替换 implementation。api scope 在 java-library plugin 中,这很容易理解,因为 application 是独立的,不会被复用,而 java-library 是可以被复用的:
arduino
apply plugin: 'java-library'
dependencies {
api 'org.apache.commons:commons-math3:3.6.1'
}
2.3 Gradle 执行阶段(Execution)
Gradle 执行阶段是构建过程的最后一步,负责真正执行项目中定义的任务。在执行阶段,Gradle 会按照任务之间的依赖关系,逐个执行任务并完成构建过程。执行阶段通常涉及编译、打包、测试和部署等具体操作,Gradle 会根据项目的配置和需求,自动化地执行这些任务。通过执行阶段,开发者可以将项目从源代码编译到最终可执行的产品,实现自动化构建和持续集成。
Gradle 是一个增量构建系统(incremental build system),如果某个 task 没有更改,它会输出 UP-TO-DATE 并跳过执行,这可以让构建更快。
以 compile task 为例,它的 input 是 source code,output 是 class files。如果本次构建时 source code 相对于上次构建而言,没有更改,并且 output 没有被删除,那么 compile task 在本次构建时就会被跳过。简言之,input 和 output 未改变,则输出 UP-TO-DATE 并跳过此 task。
clean task 用于删除 build 文件夹,如果执行了 clean 再执行 compile,由于 output 被删除了,所以 compile 又会被执行一遍。
三、其他
3.1 Kotlin DSL
Gradle 支持使用 Kotlin 语言,使用 Kotlin DSL 时,文件名不一样:
- settings.gradle -> settings.gradle.kts
- build.gradle -> build.gradle.kts
使用的语言也由 groovy 换成 kotlin。
3.2 gradle wrapper
gradle wrapper 的作用是提供一份离线的 gradle 程序,这样电脑上无需安装 gradle 也能运行一个 project。而且能避免版本不一致的问题。
项目中有一个 wrapper task,用于生成 gradle-wrapper,其中可以配置 gradleVersion:
ini
wrapper {
gradleVersion = '8.2'
}
使用 gradle wrapper 执行 clean + build 示例代码:
gradlew clean build