了解了 Gradle 的前世,现在咱们来看 Gradle 的今生,本文主要介绍 Gradle 的一些基础知识与原理,包括 Gradle 各个文件的作用,以及生命周期,构建总体流程,以及生命周期 Hook 方法等。
了解 Gradle 的这些基础原理,可以帮助我们更好的了解 Android 构建打包的过程,也方便我们利用 Gradle 生命周期做一些 Hook 工作,提升开发效率。废话不多说,见下方内容:
1. Gradle 到底是什么?
上篇,咱们了解了 Gradle
是一个依赖管理和构建自动化工具。深入点说呢,Gradle
是一个 运行在 JVM
的通用构建工具,其核心模型是一个由 Task
组成的有向无环图(Directed Acyclic Graphs)
。
2. Gradle Wrapper 是什么?
说起来我们一直在使用 Gradle
,但仔细想想我们在项目中其实没有用 gradle 命令
,而一般是使用 gradlew
命令,同时如下图所示,找遍整个项目,与 gradle
有关的就这两个文件夹,和文件: gradle-wrapper.jar、gradle-wrapper.properties、gradlew、gradle.properties、settings.gradle
以及 build.gradle
,如下图:
那么问题来了,gradlew
是什么,gradle-wrapper.jar
又是什么?
wrapper
的意思:包装。
那么可想而已,就是 gradle
的包装。其实是这样的,因为 gradle
处于快速迭代阶段,经常发布新版本,如果我们的项目直接去引用,那么更改版本等会变得无比麻烦。而且每个项目又有可能用不一样的 gradle
版本,这样去手动配置每一个项目对应的 gradle
版本就会变得麻烦,gradle
的引入本来就是想让大家构建项目变得轻松,如果这样的话,岂不是又增加了新的麻烦?
所以 android
想到了包装,引入 gradle-wrapper
,通过读取配置文件中 gradle
的版本,为每个项目自动的下载和配置 gradle
,就是这么简单。我们便不用关心如何去下载 gradle
,如何配置到项目中。
再来看下面一张图:
上面我们看到的图其实是 Gradle
提供内置的 Wrapper task
帮助我们自动生成 Wrapper
所需的目录文件。再看看我们 Android
项目里面自动生成的文件
这里 gradlew
也是一样的道理,它共有两个文件,gradlew
是在 linux
,mac
下使用的,gradlew.bat
是在 window
下使用的,提供在命令行下执行 gradle
命令的功能
至于为什么不直接执行 gradle
,而是执行 gradlew
命令呢?
因为就像 wrapper
本身的意义,gradle
命令行也是善变的,所以 wrapper
对命令行也进行了一层封装,使用同一的 gradlew
命令,wrapper
会自动去执行具体版本对应的 gradle
命令。
同时如果我们配置了全局的 gradle
命令,在项目中如果也用 gradle
容易造成混淆,而 gradlew
明确就是项目中指定的 gradle
版本,更加清晰与明确
3. gradle-wrapper.properties 的作用
首先来看,它的配置字段:
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
下面再看看每个属性的作用
字段名 | 说明 |
---|---|
distributionBase | 下载的 Gradle 压缩包解压后存储的目录 |
distributionPath | 相对于 distributionBase 的解压后的 Gradle 压缩包的路径 |
distributionUrl | Gradle 发行版压缩包的下载路径 |
zipStoreBase | 同 distributionBase 只不过是存放 zip 压缩包的 |
zipStorePath | 同 distributionPath ,只不过是存放 zip 压缩包的 |
其实我们最关心的应该是 distributionUrl
这个属性,他是下载 Gradle
的路径,它下载的东西会出现在以下的文件夹中
这个文件夹包含了各个版本你下载的 Gradle
。
问个问题:是不是大家和我一样,在“全球最大同性交友网站(github)”
上下载东西,之后运行会遇到下图中的问题:
导入项目的时候一直会停留在这个界面,这是为什么?其实原因很简单,就是你常用项目的 Gradle
版本跟你新导入项目的 Gradle
版本不一致造成的,那怎么解决?我本人自己是这么做的:
- 在能访问的情况下 ,由它自己去下载,不过下载时间有长有短,不能保证。
- 当你在公司被限网速的时候,当然也是我最常用的,就是把你最近常用项目的
gradle-wrapper.properties
文件替换掉你要导入项目的该文件,基本上我是这样解决的,当然有时候也会遇到替换掉报错的情况,不过比较少。
4. AGP到底是什么?
Gradle
本身是一个通用的构建系统, 它并不知道你要编译的是 Java
还是 C
。如果是在 Java
中需要调用 javac
将 .java
文件编译为 .class
文件, 而 C
则需要调用 gcc
将 .c
文件编译为 .o
文件. 那么这些构建流程如果让每个开发者自己去管理就太麻烦了. 所谓插件, 就是将某种类型编译的模板。
而 AGP
也就是是一系列适合 Android
开发的 Gradle
插件的集合,比如 com.android.application
等。
AGP
插件提供了 compileKotlin,compileJava,processResource
等一系列 Task
,并设置了 Task
之间的依赖关系. 同时还提供了很多可配置属性。而使用者只需要在 build script
中通过 plugins {...}
引入插件, 根据项目情况配置几个属性, 即可实现自定义的 Android 构建
。 通过 AGP
插件可以快速实现 Android
项目的构建,这就是 AGP
插件的意义,其执行过程中的 task
列表如下所示
5. gradle.properties 是什么?
除了 gradlew
与 AGP
,我们也经常会用到 gradle.properties
,我们经常在 gradle.peoperties
中定义一些统一的版本号,如 minSdkVersion,targetSdkVersion
等,然后再在各个 module
中通过 rootProject.minSdkVersion
获取以实现复用
那么问题来了:rootProject
是如何获取 gradle.properties
中定义的值的呢?
答案其实很简单,Gradle
启动时会默认读取 gradle.properties
, 并加载其中的参数。这跟我们在运行 Gradle
的时候通过命令行向其传递参数,效果是一样的。
当然不同的方式有不同的优先级,指定参数的优先级:命令行参数 > GRADLE_USER_HOME gradle.properties
文件 > 项目根目录 gradle.properties
文件。
Gradle 使用的两个目录:
Gradle 在执行过程中会涉及到两个目录, 一个是 Gradle User Home 另一个是 Project Root Directory.
Gradle User Home
User Home 中主要保存全局配置, 全局初始化脚本以及依赖的缓存和日志等文件. 如果开启 build cache 的话, 构建缓存也会存在这里共所有项目共享.
默认为: $USER_HOME/.gradle.
Project Root Directory
Project 目录则存储与当前项目构建相关的内容. 例如用于增量编译缓存.
总得来说,gradle.properties
其实就是一个参数的配置文件,与在命令行传递参数是一样的效果,因此在 Project
中可以读取到。
6. settings.gradle是什么?
当我们在某个目录执行 gradle
命令时, 约定的会从当前目录查找以下两个文件:
- settings.gradle(.kts)
- build.gradle(.kts)
我们常常会在 settings.gradle
中配置 module
,那么 settings.gradle
究竟是什么?起什么作用?
所有需要被构建的模块都需要在 settings.gradle
中注册, 因此它的作用是描述 "当前构建所参与的模块"。
settings.gradle
查找顺序为: 从前目录开始, 如果找到 settings.gradle(.kts)
则停止, 否则向父目录递归查找。
setting script
承担了统筹所有模块的重任,因此 api
主要是在操作所参与构建的模块以及管理构建过程需要的插件。
可以通过如下方式注册需要参与构建的模块,项目名称中:
代表项目的分隔符,类似路径中的 /.
如果以 :
开头则表示相对于 root project
。
include(":app", ":libs:someLibrary")
include(":anotherLibrary")
project(":anotherLibrary").projectDir = File(rootDir, "../another-library")
好了,我们说完settings.gradle文件之后就慢慢进入其他文件了,但是首先我们要解释一下什么是Groovy:
7. Groovy?
Groovy
是基于 JVM
虚拟机的一种动态语言,它的语法和 Java
非常相似,由 Java
入门学习 Groovy
基本没有障碍。Groovy
完全兼容 Java
,又在此基础上增加了很多动态类型和灵活的特性,比如支持密保,支持 DSL
,可以说它就是一门非常灵活的动态脚本语言。
一开始我总把 Gradle
和 Groovy
搞混了,现在我总把他们的关系弄清楚了。Gradle
像是一个软件,而 Groovy
就是写这个软件的语言,这就很简单明了吧。那下面我们说到的内容都是用 Groovy
语法写的,但是这个知识点就有请下一位阐释啦。
8. build.gradle 是什么?
到了我们最熟悉也是最常用的 build.gradle
了,每个模块都会有一个 build.gradle
来配置当前模块的构建信息, 根目录模块的 build.gradle
叫做 root build script
,其他子模块的 build script
叫做 module build script
。
项目构建的流程大致如下所示,其中的 init script
指 $GRADLE_USER_HOME
目录下的 init.gradle
文件,主要做一些初始化配置。
单模块构建的执行流程大致为:init script -> setting script -> build script
。
而多模块的构建流程, 比单模块多了一步:init script -> setting script -> root build script -> build script
。
一般而言, root build script
并不是一个实际的模块, 而是用于对子模块进行统一的配置, 所以 root build script
一般不会有太多的内容。
Gradle
在 Initialization
阶段还没有执行 build.gradle(.kts)
文件, 真正解析 build script
是在 Configuration
阶段。但是 build script
的执行比较特殊,它并不是简单执行所有代码,其本质是 用代码描述和配置构建规则, 然后按规则执行任务。Build script
作为整个 Gradle
中配置最复杂的脚本, 实际上仅仅做了两件事:
- 引入插件
- 配置属性
所谓引入插件如下所示,plugins
闭包中还可以通过 version
指定插件的版本, 以及 apply
来决定是否立刻应用插件:
plugins {
id("com.android.application")
id("com.dorongold.task-tree") version "1.4"
id("com.dorongold.task-tree") version "1.4" apply false
}
而所谓配置属性,实际上是对引入的插件进行配置. 原本 build script
中并没有 android {...}
这个 dsl
属性, 这是 plugin
提供的。 一旦应用了某个插件,就可以使用插件提供的 dsl
对其进行配置,从而影响该模块的构建过程。换个角度看,这些插件提供的属性配置 dsl
就相当于插件 init
函数的参数, 最终传入到插件中,当构建执行的时候就会根据配置对当前模块进行编译。
plugins {
id("com.android.application")
}
android {
compileSdkVersion(28)
defaultConfig {
....
}
}
....
9. Gradle生命周期是怎样的?
在了解了上面这些知识后,我们可以开始了解一下 Gradle
的生命周期。在了解了 Gradle
的生命周期后,我们可以对 Gradle
执行的总体流程有一个了解,也可以利用这些生命周期做一些 Hook
的操作。
不同于传统脚本的自上而下执行, 一次 Gradle
构建涉及到多个文件, 主体流程如下:
总体来说,Gradle
的执行分为三大阶段:Initialization -> Configuration -> Execution
,每个阶段都有自己的职责。
9.1 Initialization 阶段
Initialization
阶段主要目的是初始化构建,它又分为两个子过程,一个是执行 Init Script
,另一个是执行 Setting Script
。
Init script
会读取全局脚本,主要作用是初始化一些全局通用的属性,例如获取 Gradle User Home
目录,Gradle version
等。
而 Setting Script
就是我们上面提到的 settings.gradle
。
主要步骤:
1、执行 Init 脚本:Initialization Scripts[5] 会在构建最开始执行,一般用于设置全局属性、声明周期监听、日志打印等。
Gradle 支持多种配置 Init 脚本的方法,以下方式配置的所有 Init 脚本都会被执行:
• gradle 命令行指定的文件:gradle —init-script <file>
• USER_HOME/.gradle/init.gradle 文件
• USER_HOME/.gradle/init.d/ 文件夹下的 .gradle 文件
• GRADLE_HOME/init.d/ 文件夹下的 .gradle 文件
2、实例化 Settings[6] 接口实例:解析根目录下的 settings.gradle 文件,并实例化一个 Settings 接口实例;
3、执行 settings.gradle 脚本:在 settings.gradle 文件中的代码会在初始化阶段执行;
4、实例化 Project 接口实例:Gradle 会解析 include 声明的模块,并为每个模块 build.gradle 文件实例化 Project 接口实例。Gradle 默认会在工程根目录下寻找 include 包含的项目,如果你想包含其他工程目录下的项目,可以这样配置:
// 引用当前工程目录下的模块
include ':app'
// 引用其他工程目录下的模块
include 'video' // 易错点:不要加’冒号 :‘
project(:video).projectDir = new File("..\\libs\\video")
9.2 Configuration 阶段
当构建完成 Initialization
阶段后,将进入 Configuration
阶段。 这个阶段开始加载项目中所有模块的 Build Script
。所谓 "加载" 就是执行 build.gradle(.kts)
中的语句,根据脚本代码创建对应的 task
,最终根据所有 task
生成对应的依赖图。我们上面说过"Gradle 核心模型是一个 Task 组成的有向无环图(Directed Acyclic Graphs)" 吗?
这个任务依赖图就是在这个阶段生成的。
需要注意的是,Configuration
阶段各个模块的加载顺序是无序的,跟依赖关系与加入顺序都没有关系。
主要包含 3 步:
1、下载插件和依赖:Project 通常需要依赖其他插件或 Project 来完成工作,如果有需要先下载;
2、执行脚本代码:在 build.gradle 文件中的代码会在配置阶段执行;
3、构造 Task DAG:根据 Task 的依赖关系构造一个有向无环图,以便在执行阶段按照依赖关系执行 Task。
注:执行任何 Gradle 构建命令,都会先执行初始化阶段和配置阶段。
9.3 Execution 阶段
当完成任务依赖图后, Gradle
就做好了一切准备, 然后进入 Execution
阶段. 这个阶段才真正进行编译和打包动作。对于 Java
而言是调用 javac
编译源码, 然后打包成 jar
。对于 Android
而言则更加复杂些,这些差异来源于我们应用的插件。总得来说,就是开始执行 task
了。
这里有两个容易理解错误的地方:
- Task 配置代码在配置阶段执行,而 Task 动作在执行阶段执行;
- 即使执行一个 Task,整个工程的初始化阶段和所有 Project 的配置阶段也都会执行,这是为了支持执行过程中访问构建模型的任何部分。
介绍完三个生命周期阶段后,你可以通过以下 Demo 体会各个代码单元所处的执行阶段:
USER_HOME/.gradle/init.gradle
println 'init.gradle:This is executed during the initialization phase.'
settings.gradle
rootProject.name = 'basic'
println 'settings.gradle:This is executed during the initialization phase.'
build.gradle
println 'build.gradle:This is executed during the configuration phase.'
tasks.register('test') {
doFirst {
println 'build.gradle:This is executed first during the execution phase.'
}
doLast {
println 'build.gradle:This is executed last during the execution phase.'
}
// 易错点:这里在配置阶段执行
println 'build.gradle:This is executed during the configuration phase as well.'
输出:
Executing tasks: [test] in project /Users/pengxurui/workspace/public/EasyUpload
init.gradle:This is executed during the initialization phase.
settings.gradle:This is executed during the initialization phase.
> Configure project :
build.gradle:This is executed during the configuration phase.
build.gradle:This is executed during the configuration phase as well.
> Task :test
build.gradle:This is executed first during the execution phase.
build.gradle:This is executed last during the execution phase.
...
}
9.4 生命周期 Hook
Gradle
提供了丰富的生命周期 Hook
,我们可以根据我们的需要添加各种 HooK
,见下图:
根据图中生命周期的位置,可以清楚地了解到 "生命周期的最晚注册时机"。比如,settingsEvaluated
是在 setting script
被 evaluated
完毕后回调,那么在 init script
和 setting script
中注册都是没问题的。但是如果注册在 build script
中,则无法发挥作用。
同时关于生命周期 Hook
,还有下面几点需要注意:
-
projectsLoaded
之前Project
还没有创建,因此只能使用gradle
和settings
对象。
settings.gradle
// Settings 配置完毕
gradle.settingsEvaluated {
...
}
// 所有 Project 对象创建(注意:此时 build.gradle 中的配置代码还未执行)
gradle.projectsLoaded {
...
}
-
projectsLoaded
回调时已经根据setting script
创建了各个模块的Project
对象, 我们可以引用project
对象从而设置一些hook
,便是build script
还没有被配置,因此拿不到配置信息。 - 每当一个
build.gradle(.kts)
被执行完毕, 都会产生afterEvaluate
回调, 代表着project
被evaluate
完成。从此,project
对象内容完整了, 即:当前build.gradle(.kts)
中所有的配置项都能够被访问。
// 执行 build.gradle 前
project.beforeEvaluate {
...
}
// 执行 build.gradle 后
project.afterEvaluate {
...
}
除此之外,Gradle 接口也提供了配置阶段的监听:
// 执行 build.gradle 前
gradle.beforeProject { project ->
...
}
// 执行 build.gradle 后
gradle.afterProject { project ->
// 配置后,无论成功或失败
if (project.state.failure) {
println "Evaluation of $project FAILED"
} else {
println "Evaluation of $project succeeded"
}
}
// 与 project.beforeEvaluate 和 project.afterEvaluate 等价
gradle.addProjectEvaluationListener(new ProjectEvaluationListener() {
@Override
void beforeEvaluate(Project project) {
...
}
@Override
void afterEvaluate(Project project, ProjectState projectState) {
...
}
})
// 依赖关系解析完毕
gradle.addListener(new DependencyResolutionListener() {
@Override
void beforeResolve(ResolvableDependencies dependencies) {
....
}
@Override
void afterResolve(ResolvableDependencies dependencies) {
....
}
})
// Task DAG 构造完毕
gradle.taskGraph.whenReady {
}
// 与 gradle.taskGraph.whenReady 等价
gradle.addListener(new TaskExecutionGraphListener() {
@Override
void graphPopulated(TaskExecutionGraph graph) {
...
}
})
// 所有 Project 的 build.gradle 执行完毕
gradle.projectsEvaluated {
...
}
- 所有的
Project
配置结束,会回调projectsEvaluated
。 -
Gradle
的核心逻辑就是根据task
的依赖关系生成有向无环图,然后依次执行图中的task,task graph
生成后会回调graphPopulated
。 - 当所有
task
都执行完毕, 整个构建也宣告结束,这个时候会回调buildFinished
。
10. Project 核心 API
Project
可以理解为模块的构建管理器,在初始化阶段,Gradle
会为每个模块的 build.gradle
文件实例化一个接口对象。在 .gradle
脚本中编写的代码,本质上可以理解为是在一个 Project
子类中编写的。
10.1 Project API
Project
提供了一系列操作 Project
对象的 API
:
- getProject():返回当前 Project;
- getParent():返回父 Project,如果在工程 RootProject 中调用,则会返回 null;
- getRootProject():返回工程 RootProject;
- getAllprojects():返回一个 Project Set 集合,包含当前 Project 与所有子 Project;
- getSubprojects():返回一个 Project Set 集合,包含所有子 Project;
- project(String):返回指定 Project,不存在时抛出 UnKnownProjectException;
- findProject(String):返回指定 Project,不存在时返回 null;
- allprojects(Closure):为当前 Project 以及所有子 Project 增加配置;
- subprojects(Closure):为所有子 Project 增加配置。
10.2 Project 属性 API
Project
提供了一系列操作属性的 API
,通过属性 API
可以实现在Project
之间共享配置参数:
- hasProperty(String):判断是否存在指定属性名;
- property(Stirng):获取属性值,如果属性不存在则抛出 MissingPropertyException;
- findProperty(String):获取属性值,如果属性不存在则返回 null;
- setProperty(String, Object):设置属性值,如果属性不存在则抛出 MissingPropertyException。
实际上,你不一定需要显示调用这些 API
,当我们直接使用属性名时,Gradle
会帮我们隐式调用 property()
或 setProperty()
。例如:
build.gradle
name => 相当于 project.getProperty("name")
project.name = "Peng" => 相当于 project.setProperty("name", "Peng")
10.2.1 属性匹配优先级
Project 属性的概念比我们理解的字段概念要复杂些,不仅仅是一个简单的键值对。Project 定义了 4 种命名空间(scopes)的属性 —— 自有属性、Extension 属性、ext 属性、Task。当我们通过访问属性时,会按照这个优先级顺序搜索。
getProperty() 的搜索过程:
1、自有属性:Project 对象自身持有的属性,例如 rootProject 属性;
2、Extension 属性;
3、ext 属性;
4、Task:添加到 Project 上的 Task 也支持通过属性 API 访问;
5、父 Project 的 ext 属性:会被子 Project 继承,因此当 1 ~ 5 未命中时,会继续从父 Project 搜索。需要注意:从父 Project 继承的属性是只读的;
6、以上未命中,抛出 MissingPropertyException 或返回 null。
setProperty() 的搜索路径(由于部分属性是只读的,搜索路径较短):
1、自有属性。
2、ext 额外属性。
提示:其实还有 Convention 命名空间,不过已经过时了,我们不考虑。
10.2.2 Extension 扩展
Extension 扩展是插件为外部构建脚本提供的配置项,用于支持外部自定义插件的工作方式,其实就是一个对外开放的 Java Bean 或 Groovy Bean。例如,我们熟悉的 android{} 就是 Android Gradle Plugin 提供的扩展。
关于插件 Extension 扩展的更多内容,下次再讲。
10.2.3 ext 属性
Gradle 为 Project 和 Task 提供了 ext 命名空间,用于定义额外属性。如前所述,子 Project 会继承 父 Project 定义的 ext 属性,但是只读的。我们经常会在 Root Project 中定义 ext 属性,而在子 Project 中可以直接复用属性值,例如:
项目 build.gradle
ext {
kotlin_version = '1.4.31'
}
模块 build.gradle
// 如果子 Project 也定义了 kotlin_version 属性,则不会引用父 Project
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
10.3 Project 文件 API
10.3.1 文件路径
- getRootDir():Project 的根目录(不是工程根目录)
- getProjectDir():包含 build 文件夹的项目目录
- getBuildDir():build 文件夹目录
10.3.2 文件获取
- File file(Object path):获取单个文件,相对位置从当前 Project 目录开始。
- ConfigurableFileCollection files(Object... paths):获取多个文件,相对位置从当前 Project 目录开始。
def destFile = file('releases.xml')
if (destFile != null && !destFile.exists()) {
destFile.createNewFile()
}
10.3.3 文件拷贝
- copy(Closure):文件拷贝,参数闭包用于配置 CodeSpec[8] 对象。
copy {
// 来源文件
from file("build/outputs/apk")
// 目标文件
into getRootProject().getBuildDir().path + "/apk/"
exclude {
// 排除不需要拷贝的文件
}
rename {
// 对拷贝过来的文件进行重命名
}
}
10.3.4 文件遍历
- fileTree(Object baseDir):将指定目录转化为文件树,再进行遍历操作。
fileTree("build/outputs/apk") { FileTree fileTree ->
fileTree.visit { FileTreeElement fileTreeElement ->
// 文件操作
}
}
最后再来看一下,Gradle
配置详解。