理解 Gradle

在软件开发领域,构建工具是开发过程中不可或缺的一部分。它们承担着将源代码转换为可执行程序或库的重要任务,并且在自动化构建、依赖管理和持续集成方面发挥着关键作用。在众多构建工具中,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

四、参考文章

相关推荐
流水随清风7 天前
IDEA 使用 Gradle 强制清除缓存,更新快照
java·ide·gradle·intellij-idea
大耳猫21 天前
解决Error resolving plugin xxx
kotlin·gradle·android-studio
倾城半生-花落成殇22 天前
Android Studio 右侧工具栏 Gradle 不显示 Task 列表
android·ide·gradle·android studio
李艺为22 天前
RePlugin调试时执行Gradle任务rpInstallAndRunPluginDebug失败原因分析
android·java·gradle·android studio
之诚1 个月前
使用Gradle编译前端的项目
前端·vue·gradle
豪宇刘1 个月前
Gradle核心概念总结
开发语言·gradle
小墙程序员1 个月前
一文了解 Gradle 插件
android·gradle
Dnelic-1 个月前
【笔记】Android Gradle Plugin配置文件相关说明-libs.versions.toml
android·ide·笔记·gradle·plugin·版本控制
Dnelic-1 个月前
Android Studio Gradle 配置 gradle-wrapper.properties
android·ide·gradle·android studio·自学笔记
帅次1 个月前
Android CoordinatorLayout:打造高效交互界面的利器
android·gradle·android studio·rxjava·android jetpack·androidx·appcompat