请点赞,你的点赞对我意义重大,满足下我的虚荣心。
🔥 Hi,我是小彭。本文已收录到 GitHub · Android-NoteBook 中。这里有 Android 进阶成长知识体系,有志同道合的朋友,欢迎跟着我一起成长。(联系方式在 GitHub)
Gradle 作为官方主推的构建系统,目前已经深度应用于 Android 的多个技术体系中,例如组件化开发、产物构建、单元测试等。可见,要成为 Android 高级工程师 Gradle 是必须掌握的知识点。在这篇文章里,我将带你由浅入深建立 Gradle 的基本概念,涉及 Gradle 生命周期、Project、Task 等知识点,这些内容也是 Gradle 在面试八股文中容易遇见的问题。
从这篇文章开始,我将带你全面掌握 Gradle 构建系统,系列文章:
Gradle 并不仅仅是一个语言,而是一套构建工具。在早期,软件构建只有编译和打包等简单需求,但软件开发的发展,现在的构建变得更加复杂。而构建工具就是在这一背景下衍生出来的工具链,它能够帮助开发者可重复、自动化地生成目标产物。例如 Ant、Maven 和 ivy 也是历史演化过程中诞生的构建工具。
相比于早期出现的构建工具,Gradle 能够脱颖而出主要是以下优点:
Gradle 也有明显的缺点,例如:
在 Android Studio 中创建新项目时,会自动生成以下与 Gradle 相关文件。这些大家都很熟悉了,简单梳理下各个文件的作用:
. ├── a-subproject │ └── build.gradle ├── build.gradle ├── settings.gradle ├── gradle.properties ├── local.properties ├── gradle │ └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew └── gradlew.bat
Gradle Daemon 是 Gradle 3.0 引入的构建优化策略,通过规避重复创建 JVM 和内存缓存的手段提升了构建速度。 Daemon 进程才是执行构建的进程,当构建结束后,Daemon 进程并不会立即销毁,而是保存在内存中等待承接下一次构建。根据官方文档说明,Gradle Daemon 能够降低 15-75% 的构建时间。
Daemon 的优化效果主要体现在 3 方面:
相关的 Gradle 命令:
提示: 并不是所有的构建都会复用同一个 Daemon 进程,如果已存活的 Daemon 进程无法满足新构建的需求,则 Gradle 会新建一个新的 Daemon 进程。影响因素:
- Gradle 版本: 不同 Gradle 版本的构建不会关联到同一个 Daemon 进程;
- Gradle 虚拟机参数: 不满足的虚拟机参数不会关联到同一个 Daemon 进程。
Gradle Wrapper 本质是对 Gradle 的一层包装,会在执行 Gradle 构建之前自动下载安装 Gradle 环境。 在开始执行 Gradle 构建时,如果当前设备中还未安装所需版本的 Gradle 环境,Gradle Wrapper 会先帮你下载安装下来,将来其他需要这个 Gradle 版本的工程也可以直接复用。
Android Studio 默认使用 Gradle Wrapper 执行构建,你可以在设置中修改这一行为:
命令行也有区分:
gradle
:使用系统环境变量定义的 Gradle 环境进行构建;gradlew
:使用 Gradle Wrapper 执行构建。为什么 Gradle 官方从早期就专门推出一个自动安装环境工具呢,我认为原因有 2 个:
简单说下 Gradle Wrapper 相关的文件,主要有 4 个:
gradlew
才是基于 Gradle Wrapper 执行的,而使用 gradle
命令是直接基于系统安装的 Gradle 环境执行编译;distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https://services.gradle.org/distributions/gradle-6.0.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists
提示: GRADLE_USER_HOME 的默认值是
用户目录/.gradle
,可以通过系统环境变量 GRADLE_USER_HOME 修改。
Gradle 是运行在 Java 虚拟机的,gradle.properties 文件可以配置 Gradle 构建的运行环境,并且会覆盖 Android Studio 设置中的全局配置,完整构建环境配置见官方文档:Build Enviroment。常用的配置项举例:
# Gradle Daemon 开关,默认 ture org.gradle.daemon=true # 虚拟机参数 org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 # 多模块工程并行编译多个模块,会消耗更多内存 org.gradle.parallel=true
除了构建环境配置,其他配置也可以用类似的键值对方式放在 gradle.properties 中,并直接在 .gradle 文件中引用。
Groovy 是从 Java 虚拟机衍生出来的语言,由于我们都具备一定的 Java 基础,所以我们没有必要完全从零开始学习 Groovy。梳理 Groovy 与 Java 之间有差异的地方,或许是更高效的学习方式:
def
关键字声明动态类型(静态类型和动态类型的关键区别在于 ”类型检查是否倾向于在编译时执行“。例如 Java 是静态类型语言,意味着类型检查主要由编译器在编译时完成);// 使用 def 关键字 def methodName() { // Method Code } String methodName() { // Method Code }
// 省略参数类型 def methodName(param1, param2) { // Method Code } def methodName(String param1, String param2) { // Method Code }
def methodName(param1, param2 = 1) { // Method Code }
def methodName() { return "返回值" } 等价于 def methodName() { "返回值" }
// 实现 GroovyInterceptable 接口,才会把方法调用分派到 invokeMethod。 class Student implements GroovyInterceptable{ def name; def hello() { println "Hello ${name}" } @Override Object invokeMethod(String name, Object args) { System.out.println "invokeMethod : $name" } } def student = new Student(name: "Tom") student.hello() student.hello1() 输出: invokeMethod : hello invokeMethod : hello1 ------------------------------------------------------------- class Student { def name; def hello() { println "Hello ${name}" } @Override Object methodMissing(String name, Object args) { System.out.println "methodMissing : $name" } } def student = new Student(name: "Tom") student.hello() student.hello1() 输出: Hello Tom methodMissing hello1
Groovy 支持通过 [] 关键字定义 List 列表或 Map 集合:
// 列表 def list = [10, 11, 12] list.each { value -> } list.eachWIthIndex { value, index -> } // 集合 def map = [’name’:’Tom’, ‘age’:18] map.each { key, value -> } map.eachWithIndex { entry, index -> } map.eachWithIndex { key, value, index -> }
Groovy 闭包是一个匿名代码块,可以作为值传递给变量或函数参数,也可以接收参数和提供返回值,形式上与 Java / Kotlin 的 lambda 表达式类似。例如以下是有效的闭包:
{ 123 } { -> 123 } { println it } { it -> println it } { name -> println name } { String x, int y -> println "hey ${x} the value is ${y}" }
groovy.lang.Closure
的实例,使得闭包可以像其他类型的值一样复制给变量。例如:Closure c = { 123 } // 当然也可以用 def 关键字 def c = { 123 }
def c = { 123 } // 通过 Closure#call() 调用 c.call() // 直接通过变量名调用 c()
→
),Groovy 总是带有隐式添加一个参数 it。如果调用者没有使用任何实参,则 it 为空。当你需要声明一个不接收任何参数的闭包,那么必须用显式的空参数列表声明。例如:// 带隐式参数 it def greeting = { "Hello, $it!" } assert greeting('Patrick') == 'Hello, Patrick!' // 不带隐式参数 it def magicNumber = { -> 42 } // error 不允许传递参数 magicNumber(11)
def methodName(String param1, Closure closure) { // Method Code } // 调用: methodName("Hello") { // Closure Code }
class Person { String name } def p = new Person(name:'Igor') def cl = { // 相当于 delegate.name.toUpperCase() name.toUpperCase() } cl.delegate = p assert cl() == 'IGOR'
闭包定义了多种解析策略,可以通过 Closure#resolveStrategy=Closure.DELEGATE_FIRST
修改:
Gradle 将构建划分为三个阶段: 初始化 - 配置 - 执行 。理解构建生命周期(Gradle Build Lifecycle)非常重要,否则你可能连脚本中的每个代码单元的执行时机都搞不清楚。
由于 Gradle 支持单模块构建或多模块构建,因此在初始化阶段(Initialization Phase),Gradle 需要知道哪些模块将参与构建。主要包含 4 步:
gradle —init-script <file>
settings.gradle
文件,并实例化一个 Settings 接口实例;include
声明的模块,并为每个模块 build.gradle
文件实例化 Project 接口实例。Gradle 默认会在工程根目录下寻找 include 包含的项目,如果你想包含其他工程目录下的项目,可以这样配置:// 引用当前工程目录下的模块 include ':app' // 引用其他工程目录下的模块 include 'video' // 易错点:不要加’冒号 :‘ project(:video).projectDir = new File("..\libs\video")
提示: 模块 build.gradle 文件的执行顺序和 include 顺序没有关系。
配置阶段(Configuration Phase)将执行 build.gradle 中的构建逻辑,以完成 Project 的配置。主要包含 3 步:
提示: 执行任何 Gradle 构建命令,都会先执行初始化阶段和配置阶段。
在配置阶段已经构造了 Task DAG,执行阶段(Execution Phase)就是按照依赖关系执行 Task。这里有两个容易理解错误的地方:
原文: This means that when a single task, from a single project is requested, all projects of a multi-project build are configured first. The reason every project needs to be configured is to support the flexibility of accessing and changing any part of the Gradle project model.
介绍完三个生命周期阶段后,你可以通过以下 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. ...
提示: Task 在执行阶段执行有一个特例,即通过 Project#defaultTasks 指定默认任务,会在配置阶段会执行,见 第 6.2 节 ,了解即可。
Gradle 提供了一系列监听构建生命周期流程的接口,大部分的节点都有直接的 Hook 点,这里我总结一些常用的:
Gradle 接口提供了监听 Settings 初始化阶段的方法:
settings.gradle
// Settings 配置完毕 gradle.settingsEvaluated { ... } // 所有 Project 对象创建(注意:此时 build.gradle 中的配置代码还未执行) gradle.projectsLoaded { ... }
Project 接口提供了监听当前 Project 配置阶段执行的方法,其中 afterEvaluate 常用于在 Project 配置完成后继续增加额外的配置,例如 Hook 构建过程中的 Task。
// 执行 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 { ... }
Gradle 接口提供了执行阶段的监听:
gradle.addListener(new TaskExecutionListener() { // 执行 Task 前 @Override void beforeExecute(Task task) { ... } // 执行 Task 后 @Override void afterExecute(Task task, TaskState state) { ... } }) gradle.addListener(new TaskActionListener() { // 开始执行 Action 列表前,回调时机略晚于 TaskExecutionListener#beforeExecute @Override void beforeActions(Task task) { ... } // 执行 Action 列表完毕,回调时机略早于 TaskExecutionListener#afterExecute @Override void afterActions(Task task) { ... } }) // 执行 Task 前 gradle.taskGraph.beforeTask { Task task -> } // 执行 Task 后 gradle.taskGraph.afterTask { Task task, TaskState state -> if (state.failure) { println "FAILED" } else { println "done" } }
TaskContainer 接口提供了监听 Task 添加的方法,可以在 Task 添加到 Project 时收到回调:
tasks.whenTaskAdded { task -> }
当所有 Task 执行完毕,意味着构建结束:
gradle.buildFinished { ... }
Project 可以理解为模块的构建管理器,在初始化阶段,Gradle 会为每个模块的 build.gradle 文件实例化一个接口对象。在 .gradle 脚本中编写的代码,本质上可以理解为是在一个 Project 子类中编写的。
Project 提供了一系列操作 Project 对象的 API:
Project 提供了一系列操作属性的 API,通过属性 API 可以实现在 Project 之间共享配置参数:
实际上,你不一定需要显示调用这些 API,当我们直接使用属性名时,Gradle 会帮我们隐式调用 property() 或 setProperty()。例如:
build.gradle
name => 相当于 project.getProperty("name") project.name = "Peng" => 相当于 project.setProperty("name", "Peng")
4.2.1 属性匹配优先级
Project 属性的概念比我们理解的字段概念要复杂些,不仅仅是一个简单的键值对。Project 定义了 4 种命名空间(scopes)的属性 —— 自有属性、Extension 属性、ext 属性、Task。 当我们通过访问属性时,会按照这个优先级顺序搜索。
getProperty()
的搜索过程:
setProperty()
的搜索路径(由于部分属性是只读的,搜索路径较短):
提示: 其实还有 Convention 命名空间,不过已经过时了,我们不考虑。
4.2.2 Extension 扩展
Extension 扩展是插件为外部构建脚本提供的配置项,用于支持外部自定义插件的工作方式,其实就是一个对外开放的 Java Bean 或 Groovy Bean。例如,我们熟悉的 android{}
就是 Android Gradle Plugin 提供的扩展。
关于插件 Extension 扩展的更多内容,见下一篇文章。
4.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"
4.3.1 文件路径
4.3.2 文件获取
def destFile = file('releases.xml') if (destFile != null && !destFile.exists()) { destFile.createNewFile() }
4.3.3 文件拷贝
copy { // 来源文件 from file("build/outputs/apk") // 目标文件 into getRootProject().getBuildDir().path + "/apk/" exclude { // 排除不需要拷贝的文件 } rename { // 对拷贝过来的文件进行重命名 } }
4.3.4 文件遍历
fileTree("build/outputs/apk") { FileTree fileTree -> fileTree.visit { FileTreeElement fileTreeElement -> // 文件操作 } }
Project 的构建逻辑由一系列 Task 的组成,每个 Task 负责完成一个基本的工作,例如 Javac 编译 Task、资源编译 Task、Lint 检查 Task,签名 Task等。在构建配置阶段,Gradle 会根据 Task 的依赖关系构造一个有向无环图,以便在执行阶段按照依赖关系执行 Task。
Gradle 支持两种创建简单 Task 的语法:
// 创建名为 MyTask 的任务 task MyTask(group: "MyGroup") { // Task 配置代码 }
// 创建名为 MyTask 的任务 project.tasks.create(name: "MyTask") { // Task 配置代码 }
除了简单创建 Task 的方式,我们还可以自定义 Task 类型,Gradle 将这类 Task 称为增强 Task。增强 Task 的可重用性更好,并且可以通过暴露属性的方式来定制 Task 的行为。
class CustomTask extends DefaultTask { final String message final int number }
@Inject
注解修饰构造器。class CustomTask extends DefaultTask { final String message final int number @Inject CustomTask(String message, int number) { this.message = message this.number = number } }
// 第二个参数为 Task 类型 tasks.register('myTask', CustomTask, 'hello', 42)
可以获取 TaskContainer 中已创建的任务,对于通过 register 注册的任务会在这个时机实例化。例如:
// 获取已创建的 Task project.MyTask.name => 等同于 project.tasks.getByName("MyTask").name
设置 Task 属性的语法主要有三种:
task MyTask(group: "MyGroup")
task MyTask { group = "MyGroup" => 等同于 setGroup("MyGroup") }
task MyTask(group:"111") { ext.goods = 2 } ext.goods = 1 println MyTask.good 输出:2
Task 常用的自有属性如下:
属性 | 描述 |
---|---|
name | Task 标识符,在定义 Task 时指定 |
group | Task 所属的组 |
description | Task 的描述信息 |
type | Task类型,默认为 DefaultTask |
actions | 动作列表 |
dependsOn | 依赖列表 |
注意事项:
name
,否则在一些版本的 Android Studio 中会被截断,导致不兼容;group
属性对 Task 进行分组显示。其中, Tasks 组为 Root Project 中的 Task,其他分组为各个 Project 中的 Task,未指定 group 的 Task 会分配在 other 中。build.gradle
defaultTasks 'hello','hello2' task hello { println "defaultTasks hello" } task hello2 { println "defaultTasks hello2" } 输出: > Configure project :easyupload defaultTasks hello defaultTasks hello2 --afterEvaluate-- --taskGraph.whenReady--
每个 Task 内部都保持了一个 Action 列表 actions
,执行 Task 就是按顺序执行这个列表,Action 是比 Task 更细的代码单元。Task 支持添加多个动作,Task 提供了两个方法来添加 Action:
task MyTask MyTask.doFirst{ println "Action doFirst 1" } MyTask.doFirst{ println "Action doFirst 2" } MyTask.doLast{ println "Action doLast 1" } 执行 MyTask 输出: Action doFirst 2 Action doFirst 1 Action doLast 1
对于自定义 Task,还可以通过 @TaskAction
注解添加默认 Action。例如:
abstract class CustomTask extends DefaultTask { @TaskAction def greet() { println 'hello from GreetingTask' } }
并不是所有 Task 都会被执行,Gradle 提供了多个方法来控制跳过 Task 的执行:
剩下两种方式允许在执行 Task 的过程中中断执行:
通过建立 Task 的依赖关系可以构建完成的 Task 有向无环图:
// 通过属性设置依赖列表 task task3(dependsOn: [task1, task2]) { } // 添加依赖 task3.dependsOn(task1, task2) 依赖关系:task3 依赖于 [task1, task2],在执行 task3 前一定会执行 task1 和 task2
在某些情况下,控制两个任务的执行顺序非常有用,而不会在这些任务之间引入显式依赖关系,可以理解为弱依赖。 任务排序和任务依赖关系之间的主要区别在于,排序规则不影响将执行哪些任务,只影响任务的执行顺序。
task3 mustRunAfter(task1, task2) task3 shouldRunAfter(task1, task2) 依赖关系:无,在执行 task3 前不一定会执行 task1 和 task2 顺序关系:[task1, task2] 优先于 task3
给一个 Task 添加 Finalizer 终结器任务后,无论 Task 执行成功还是执行失败,都会执行终结器,这对于需要在 Task 执行完毕后清理资源的情况非常有用。
// taskY 是 taskX 的终结器 taskX finalizedBy taskY
任何构建工具都会尽量避免重复执行相同工作,这一特性称为 Incremental Build 增量构建,这一特性能够节省大量构建时间。例如编译过源文件后就不应该重复编译,除非发生了影响输出的更改(例如修改或删除源文件)。
Gradle 通过对比自从上一次构建之后,Task 的 inputs
和 outputs
是否变化,来决定是否跳过执行。如果相同,则 Gralde 认为 Task 是最新的,从而会跳过执行。在 Build Outputs 中看到 Task 名称旁边出现 UP-TO-DATE
标志,即说明该 Task 是被跳过的。例如:
> Task :easyupload:compileJava NO-SOURCE > Task :easyupload:compileGroovy UP-TO-DATE > Task :easyupload:pluginDescriptors UP-TO-DATE > Task :easyupload:processResources UP-TO-DATE > Task :easyupload:classes UP-TO-DATE > Task :easyupload:jar UP-TO-DATE > Task :easyupload:uploadArchives
那么,在定义 Task 的输入输出时,要遵循一个原则:如果 Task 的一个属性会影响输出,那么应该将该属性注册为输入,否则会影响 Task 执行;相反,如果 Task 的一个属性不会影响输出,那么不应该将该属性注册为输入,否则 Task 会在不必要时执行。
大多数情况下,Task 需要接收一些 input 输入,并生成一些 output 输出。例如编译任务,输入是源文件,而输出是 Class 文件。Task 使用 TaskInputs 和 TaskOutputs 管理输入输出:
对于 Task 的输入输出,我们用面向对象的概念去理解是没问题的。如果我们把 Task 理解为一个函数,则 Task 的输入就是函数的参数,而 Task 的输出就是函数的返回值。在此理解的基础上,再记住 2 个关键点:
Task 支持三种形式的输入:
public abstract class ProcessTemplates extends DefaultTask { @Input public abstract Property<TemplateEngineType> getTemplateEngine(); @InputFiles public abstract ConfigurableFileCollection getSourceFiles(); @Nested public abstract TemplateData getTemplateData(); @OutputDirectory public abstract DirectoryProperty getOutputDir(); @TaskAction public void processTemplates() { // ... } } public abstract class TemplateData { @Input public abstract Property<String> getName(); @Input public abstract MapProperty<String, String> getVariables(); }
通过注解方式注册输入输出时,Gradle 会在配置阶段会对属性值进行检查。如果属性值不满足条件,则 Gradle 会抛出 TaskValidationException
异常。特殊情况时,如果允许输入为 null 值,可以添加 @Optional
注解表示输入可空。
到这里,Gradle 基础的部分就讲完了,下一篇文章我们来讨论 Gradle 插件。提个问题,你知道 Gradle 插件和 .gradle 文件有区别吗?关注我,带你了解更多。