请点赞,你的“赞”对我来说意义重大,满足了我的虚荣心。
大家好,我是小鹏。本文包含在GitHub 和Android-NoteBook 上。这是Android的高级成长知识系统。欢迎志同道合的朋友与我一起成长。 (联系信息位于GitHub)
前言
作为官方构建系统,Gradle 被深度应用于Android 中的多个技术系统,包括组件开发、产品构建和单元测试。 Gradle原来是成为一名高级Android工程师必须掌握的知识点。本文由浅入深地介绍了Gradle 的基本概念,包括Gradle 生命周期、项目和任务等知识点。这些也是Gradle 可能遇到的问题。采访分为8 部分。
从本文开始,我们将向您介绍一系列文章,帮助您完全掌握Gradle 构建系统。
1. Gradle 基础知识2. Gradle 插件3. Gradle 依赖管理4. APG 转换
1. 认识 Gradle
Gradle 不仅仅是一种语言,它还是一组构建工具。早期的软件构建有编译和打包等简单的要求,但随着软件开发的发展,现代构建变得更加复杂。构建工具就是在此背景下衍生出来的帮助开发者重复、自动生成目标产品的工具链。例如Ant、Maven、ivy也是在历史演变过程中出现的构建工具。
1.1 Gradle 的优缺点
与之前的构建工具相比,Gradle 脱颖而出主要得益于以下优点:
Expressive DSL:Gradle 构建脚本使用基于Groovy 的DSL 特定于域的语言,而不是传统的XML 文件。这比Maven 等构建系统更简洁。基于Java虚拟机。 Groovy 语言基于Java 虚拟机。这支持Gradle。使用Java/Kotlin 代码编写构建脚本可以缩短Gradle 的学习曲线,因为您只需学习一小部分Groovy 语法即可启动Gradle 脚本。 Gradle 有一个胜过配置的原则。与构建像Ant 这样的系统相比,优先选择约定而不是配置,或者提供具有默认值的属性,是一种更简单的入门方法。开发Gradle 插件时也应该遵循这个原则。 Gradle 也有一些明显的缺点:
向后兼容性弱:Gradle 是一个快速发展的工具,新版本经常会破坏向后兼容性。有经验的同学都知道,项目用低版本的Gradle可以编译,但用新版本的Gradle可能会编译失败。
1.2 Gradle 工程的基本结构
当您在Android Studio 中创建新项目时,会自动生成以下Gradle 相关文件。这些都是大家都熟悉的事情。我们简单总结一下各个文件的功能。
. a-subproject build.gradle build.gradle settings.gradle gradle.properties local.properties gradle 包装器 gradle-wrapper.jar gradle-wrapper.properties gradlew gradlew.batsettings.gradle 文件:用于确定哪些模块参与构建。用于定义项目级的build.gradle文件:所有子模块公共配置参数。模块级build.gradle文件:用于定义子模块的配置参数,可以覆盖项目级build.gradle文件中定义的配置。安装项目脚本所需的Gradle环境; gradle.properties:作为项目级Gradle配置项,覆盖全局配置项; local.properties:用于配置项目的私有属性,如SDK安装目录。 local.properties 通常不会添加到版本控制中。
1.3 Gradle 中的重要概念
Gradle:提供核心构建过程,但没有具体的构建逻辑。 Gradle插件:Gradle提供了一套核心构建机制,Gradle插件是运行在该机制上的特定构建逻辑。与.gradle 文件基本上没有区别。比如我们熟悉的Android构建流程就是Android Gradle插件引入的构建逻辑。 Gradle 守护进程:用于加速构建的后台进程。 Gradle 包装器:添加自动下载功能。安装Gradle环境;环境变量GRADLE:用于定义Gradle安装目录。
1.4 Gradle Daemon
Gradle 守护进程是Gradle 3.0 策略中引入的构建优化,它通过避免重复的JVM 和内存缓存创建来提高构建速度。守护进程是执行构建的进程。一旦构建完成,守护进程不会立即销毁,而是保留在内存中等待下一次构建。根据官方文档,Gradle Daemon 可以减少15-75% 的构建时间。
守护进程的优化效果主要体现在三个方面:
1.减少JVM虚拟机启动时间。无需重复创建。 2. JIT编译:守护进程执行JIT编译。这提高了后续构建的字节码执行效率。构建流程资源或任务输入和输出存储在内存中,可以在后续构建中重用。相关Gradle 命令:
gradle --status:显示有关幸存守护进程的信息。 gradle --stop:停止所有守护进程。提示:并非所有构建都重用相同的守护进程。如果现有的守护进程无法满足新构建的需求,Gradle 会创建一个新的守护进程。影响因素:
Gradle 版本:使用不同Gradle 版本的构建不绑定到相同的守护进程。 Gradle虚拟机参数:不满足的虚拟机参数不与同一个守护进程关联。
1.5 Gradle Wrapper
Gradle Wrapper 本质上是Gradle 的打包层,它在运行Gradle 构建之前自动下载并安装Gradle 环境。当您启动Gradle 构建时,Gradle Wrapper 首先下载并安装所需版本的Gradle 环境(如果当前设备上尚未安装)。以后其他需要这个Gradle版本的项目可以直接复用它。
Android Studio 默认使用Gradle Wrapper 来执行构建。可以在设置中更改此行为。
命令行也有区别。
gradle:使用系统环境变量定义的Gradle环境进行构建。 gradlew:使用Gradle Wrapper 运行构建。为什么Gradle从一开始就正式发布一个自动安装环境工具?我认为有两个原因。
确保Gradle版本准确性:鉴于Gradle向后兼容性较弱,Gradle Wrapper允许您从项目级别修复项目所需的Gradle版本,从而更容易在其他计算机上使用同一项目,并实现准确且可重复的可移植性。构建;减少手动安装Gradle环境的工作量:仅Gradle 4到Gradle 7就有十几个版本,并且可以通过使用Gradle Wrapper手动安装环境来确定每个项目所需的Gradle版本,从而减少需要。用于安装。简而言之,有四个与Gradle Wrapper 相关的文件。
gradlew gradlew.bat:在Linux 或Mac 上可以使用用于运行构建作为Gradle 包装器的shell 脚本,在Windows 上可以使用批处理脚本。换句话说,在命令行使用gradlew 是基于Gradle Wrapper,而使用gradle 命令是直接基于系统上安装的Gradle 环境。 gradle-wrapper.jar:负责下载和安装Gradle环境脚本。 gradle-wrapper.properties:Gradle 包装器配置文件。它的主要功能是确定Gradle版本和安装目录: distributionBase + distributionPath:指定Gradle环境的安装路径。 distributionUrl:指定Gradle版本的下载地址。该参数允许您配置项目所需的Gradle 版本。 distributionBase=GRADLE_USER_HOMEdistributionPath=wrapper/distsdistributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zipzipStoreBase=GRADLE_USER_HOMEzipStorePath=wrapper/dists 提示: GRADLE_USER_HOME 的默认值为user directory/.gradle 。这可以通过系统环境变量GRADLE_USER_HOME 进行更改。
1.6 gradle.properties 构建环境配置
Gradle 在Java 虚拟机中运行。 gradle.properties文件可以配置Gradle构建的执行环境,完整的构建环境配置请参考官方文档Build Environments。常用配置项示例:
# Gradle 守护进程开关,默认tureorg.gradle.daemon=true # 虚拟机参数org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8# 在多模块项目中并行编译多个模块会消耗更多内存。除了gradle.Parallel=true 构建环境配置之外,您还可以在gradle.properties 中放置具有类似键值对的其他配置,并直接在.gradle 文件中引用它们。
2. Groovy 必知必会
Groovy 是一种源自Java 虚拟机的语言。每个人都有基本的Java基础,所以没有必要完全从头开始学习Groovy。理清Groovy 和Java 之间的差异可能是一种更有效的学习方式。
2.1 一些小差异
分号:语句不能以分号结尾;public:默认访问修饰符为public;getter/setter:Groovy 在访问obj.field/obj 时使用相应的.field 创建getter/setter 方法。="" 实际上访问getField() 和setField("")。支持静态和动态类型。 Groovy既支持Java中的静态类型,也支持使用def关键字声明的动态类型(静态类型和动态类型的主要区别在于,例如Java是静态类型语言,类型检查主要由编译器在编译时完成)。 字符串:Groovy 支持三种定义字符串的格式。引号和三引号单引号:类似于Java 中的双引号字符串的纯字符串;双引号:通过三引号内的$ 关键字直接引用变量值。
2.2 函数
函数定义:Groovy 支持使用返回类型或def 关键字定义函数。如果使用def 关键字定义的函数没有返回值,则默认返回null。示例: //使用def 关键字def methodName() { //方法代码}String methodName() { //方法代码} 参数名称:Groovy 不支持指定参数类型。示例: //省略参数类型def methodName(param1, param2) { //方法代码}def methodName(String param1, String param2) { //方法代码}默认参数:Groovy 支持使用默认值作为函数参数。规格。默认参数必须位于参数列表的末尾。示例: def methodName(param1, param2=1) { //方法代码} 返回值:return 是可选的,默认为语句最后一行的值。示例:def methodName() { return '返回值'} 等价于def methodName() { '返回值'}。 invokeMethod methodMissing:invokeMethod:必须实现GroovyInterceptable 才能分派对象上的所有方法调用(包括已定义和未定义的方法)。 Interface; methodMissing:分派对对象定义的方法的所有调用。 //只有实现了GroovyInterceptable 接口,才能将方法调用分派到invokeMethod。 class Student 实现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 : helloinvokeMethod : hello1-------------------- -- --- ---------------------------------class Student { def name; def hello() { println '你好。 ${name}' } @Override Object methodMissing(String name, Object args) { System.out.println 'methodMissing : $name' }}def Student=new Student(name: 'Tom')student.hello()student.hello1 () 输出:Hello TommethodMissing hello1
2.3 集合
Groovy 支持使用[] 关键字定义List 列表或Map 集合。
列表: 示例:def list=[1, 2, 3, 4] 集合: 示例:def map=['name':'Tom', 'age':18], 空集[:] 范围: 示例:def range=1. 10 遍历: //列表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 -}
2.4 闭包
Groovy 闭包是可以传递的匿名代码块。您还可以接受参数作为变量或函数参数值,并以类似于Java/Kotlin lambda 表达式的格式提供返回值。例如,以下是有效的闭包:
{ 123 } { - 123 } { println it }{ it - println it }{ name - println name } { String x, int y - println 'hey ${x} value is ${y}' } 闭包类型:Groovy Define闭包作为groovy.lang.Closure 的实例,以便您可以像任何其他类型的值一样将闭包复制到变量。例如: Closure c={ 123 }//当然,你也可以使用def 关键字def c={ 123 } 来调用闭包。闭包可以像方法一样被调用,这可以通过Closure#call() 来完成。或者通过变量直接退出。示例: def c={ 123 } //通过Closure#call() 调用c.call()。 //通过变量名直接调用c()。 隐式参数:默认情况下,闭包至少有一个形式参数。闭包不明确当您定义参数列表(使用)时,Groovy 总是隐式添加参数。如果调用者不使用参数,则它为空。如果需要声明一个不接受参数的闭包,则必须使用空参数列表显式声明它。示例: //带隐式参数itdefgreeting={ 'Hello, $it!' //不带隐式参数itdef magicNumber={ - 42 }//错误无法通过参数magicNumber(11)。 简化闭包参数:函数的最后一个参数可以在调用时简化: def methodName(String param1, Closure Closure) {//方法代码}//Call:methodName('Hello') {//闭包代码}this, owner,delegate:闭包委托是Groovy闭包和Java Lambda最大的区别,通过改变闭包可以实现灵活多样的DSL。代表团。首先,了解闭包内的三个变量。 this:定义闭包的外部类。 this 必须指向一个类对象。 Owner:定义闭包的外部对象。所有者可以是类对象或闭包的外层;委托:默认情况下,委托相当于所有者。 this 和Owner 的语义无法更改,但delegate 可以。闭包委托策略:在闭包中,如果属性没有显式声明接收者对象,则通过闭包代理解析策略找到定义的对象。例如: ')def cl={//delegate.name.toUpperCase()name.toUpperCase()}cl.delegate=passert cl()=='IGOR' 闭包定义了可以通过闭包传递的不同解析策略。 #solveStrategy=Closure.DELEGATE_FIRST 更改:
Closure.OWNER_FIRST(默认):首先搜索所有者对象,然后搜索代理对象。 Closure.DELEGATE_FIRST:首先搜索委托对象,然后搜索所有者对象。 Closure.OWNER_ONLY:仅搜索所有者。 Object; Closure.DELEGATE_ONLY:仅在委托对象内搜索; Closure.TO_SELF:仅在闭包本身内搜索
3. Gradle 构建生命周期
Gradle 将其分为三个阶段:初始化、配置和执行。了解Gradle 构建生命周期非常重要。如果你不理解它,你甚至可能不知道脚本中的每个代码单元何时被执行。
3.1 初始化阶段
Gradle 支持单模块或多模块构建,因此在初始化阶段Gradle 需要知道哪些模块将参与构建。主要涉及四个步骤:
1. 运行初始化脚本。初始化脚本在构建开始时运行,通常用于配置全局属性、生命周期监控、日志记录等。 Gradle 支持多种方式来配置Init 脚本。所有通过以下方式配置的Init 脚本都会被执行: gradle命令行指定的文件: gradle —init-script fileUSER_HOME/.gradle/init.gradle 文件USER_HOME/.gradle/init。 d/文件夹中的.gradle文件GRADLE_HOME/init.d/文件夹中的.gradle文件2.实例化设置界面实例:解析位于根目录的settings.gradle文件,实例化设置界面。
实例;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")提示: 模块 build.gradle 文件的执行顺序和 include 顺序没有关系。
3.2 配置阶段
配置阶段(Configuration Phase)将执行 build.gradle 中的构建逻辑,以完成 Project 的配置。主要包含 3 步:
1、下载插件和依赖: Project 通常需要依赖其他插件或 Project 来完成工作,如果有需要先下载;2、执行脚本代码: 在 build.gradle 文件中的代码会在配置阶段执行;3、构造 Task DAG: 根据 Task 的依赖关系构造一个有向无环图,以便在执行阶段按照依赖关系执行 Task。提示: 执行任何 Gradle 构建命令,都会先执行初始化阶段和配置阶段。
3.3 执行阶段
在配置阶段已经构造了 Task DAG,执行阶段(Execution Phase)就是按照依赖关系执行 Task。这里有两个容易理解错误的地方:
1、Task 配置代码在配置阶段执行,而 Task 动作在执行阶段执行;2、即使执行一个 Task,整个工程的初始化阶段和所有 Project 的配置阶段也都会执行,这是为了支持执行过程中访问构建模型的任何部分。原文: 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/EasyUploadinit.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 :testbuild.gradle:This is executed first during the execution phase.build.gradle:This is executed last during the execution phase....提示: Task 在执行阶段执行有一个特例,即通过 Project#defaultTasks 指定默认任务,会在配置阶段会执行,见 第 6.2 节 ,了解即可。
3.4 生命周期监听
Gradle 提供了一系列监听构建生命周期流程的接口,大部分的节点都有直接的 Hook 点,这里我总结一些常用的:
1、监听初始化阶段Gradle 接口提供了监听 Settings 初始化阶段的方法:
settings.gradle
// Settings 配置完毕gradle.settingsEvaluated {...}// 所有 Project 对象创建(注意:此时 build.gradle 中的配置代码还未执行)gradle.projectsLoaded {...}2、监听配置阶段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 { ...}3、监听执行阶段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" }}4、监听 Task 创建TaskContainer 接口提供了监听 Task 添加的方法,可以在 Task 添加到 Project 时收到回调:
tasks.whenTaskAdded { task ->}5、监听构建结束当所有 Task 执行完毕,意味着构建结束:
gradle.buildFinished { ...}
4. Project 核心 API
Project 可以理解为模块的构建管理器,在初始化阶段,Gradle 会为每个模块的 build.gradle 文件实例化一个接口对象。在 .gradle 脚本中编写的代码,本质上可以理解为是在一个 Project 子类中编写的。
4.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 增加配置。
4.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")4.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 命名空间,不过已经过时了,我们不考虑。
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 属性,则不会引用父 Projectimplementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
4.3 Project 文件 API
4.3.1 文件路径
getRootDir(): Project 的根目录(不是工程根目录)getProjectDir(): 包含 build 文件夹的项目目录getBuildDir(): build 文件夹目录4.3.2 文件获取
File file(Object path): 获取单个文件,相对位置从当前 Project 目录开始ConfigurableFileCollection files(Object... paths): 获取多个文件,相对位置从当前 Project 目录开始def destFile = file('releases.xml')if (destFile != null && !destFile.exists()) { destFile.createNewFile()}4.3.3 文件拷贝
copy(Closure): 文件拷贝,参数闭包用于配置 CodeSpec 对象copy { // 来源文件 from file("build/outputs/apk") // 目标文件 into getRootProject().getBuildDir().path + "/apk/" exclude { // 排除不需要拷贝的文件 } rename { // 对拷贝过来的文件进行重命名 }}4.3.4 文件遍历
fileTree(Object baseDir): 将指定目录转化为文件树,再进行遍历操作fileTree("build/outputs/apk") { FileTree fileTree -> fileTree.visit { FileTreeElement fileTreeElement -> // 文件操作 }}
5. Task 核心 API
Project 的构建逻辑由一系列 Task 的组成,每个 Task 负责完成一个基本的工作,例如 Javac 编译 Task、资源编译 Task、Lint 检查 Task,签名 Task等。在构建配置阶段,Gradle 会根据 Task 的依赖关系构造一个有向无环图,以便在执行阶段按照依赖关系执行 Task。
5.1 创建简单 Task
Gradle 支持两种创建简单 Task 的语法:
1、通过 task 关键字:// 创建名为 MyTask 的任务task MyTask(group: "MyGroup") { // Task 配置代码}2、通过 TaskContainer 方法: 通过 Project 的 TaskContainer 属性,可以创建 Task,分为热创建和懒创建:Task create(String, Closure) 热创建: 立即实例化 Task 对象;TaskProvider register(String, Closure) 懒创建: 注册 Task 构造器,但不会实例化对象。创建 Task 操作会延迟到访问该 Task 时,例如通过 TaskProvider#get() 或 TaskContainer#getByName()。// 创建名为 MyTask 的任务project.tasks.create(name: "MyTask") { // Task 配置代码}
5.2 创建增强 Task(自定义 Task 类型)
除了简单创建 Task 的方式,我们还可以自定义 Task 类型,Gradle 将这类 Task 称为增强 Task。增强 Task 的可重用性更好,并且可以通过暴露属性的方式来定制 Task 的行为。
1、DefaultTask: 自定义 Task 必须继承 DefaultTask。class CustomTask extends DefaultTask { final String message final int number}2、带参数创建 Task: 除了可以在创建 Task 后配置属性值,我们也可以在调用 TaskContainer#create() 时传递构造器参数。为了将值传递给任务构造函数,必须使用 @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)
5.3 获取已创建 Task
可以获取 TaskContainer 中已创建的任务,对于通过 register 注册的任务会在这个时机实例化。例如:
Task getByName(String): 获取 Task,如果 Task 不存在则抛出 UnKnownDomainObjectException;Task findByName(String): 获取 Task,如果 Task 不存在则返回 null。// 获取已创建的 Taskproject.MyTask.name => 等同于 project.tasks.getByName("MyTask").name
5.4 设置 Task 属性
设置 Task 属性的语法主要有三种:
1、在创建 Task 时设置task MyTask(group: "MyGroup")2、通过 setter 方法设置task MyTask {group = "MyGroup" => 等同于 setGroup("MyGroup")}3、通过 ext 额外属性设置: Task 也支持与 Project 类似的额外属性。例如:task MyTask(group:"111") { ext.goods = 2}ext.goods = 1println MyTask.good输出:2Task 常用的自有属性如下:
属性
描述
name
Task 标识符,在定义 Task 时指定
group
Task 所属的组
description
Task 的描述信息
type
Task类型,默认为 DefaultTask
actions
动作列表
dependsOn
依赖列表
注意事项:
严格避免使用带空格的 Task name,否则在一些版本的 Android Studio 中会被截断,导致不兼容;Android Studio 的 Gradle 面板会按照 group 属性对 Task 进行分组显示。其中, Tasks 组为 Root Project 中的 Task,其他分组为各个 Project 中的 Task,未指定 group 的 Task 会分配在 other 中。
5.5 执行 Task
1、命令行: gradlew :[模块名]:[任务名],例如:gradlew -q :app:dependencies2、IDE 工具: 通过 IDE 提供的用户界面工具执行,例如 Gradle 面板或绿色三角形,支持普通执行和调试执行;3、默认任务: 通过 Project#defaultTasks 可以指定 Project 配置阶段的默认任务,在配置阶段会执行(这说明 Task 是有可能在配置阶段执行的,了解即可,不用钻牛角尖)。build.gradle
defaultTasks 'hello','hello2'task hello { println "defaultTasks hello"}task hello2 { println "defaultTasks hello2"}输出:> Configure project :easyuploaddefaultTasks hellodefaultTasks hello2--afterEvaluate----taskGraph.whenReady--
5.6 Task Action 动作
每个 Task 内部都保持了一个 Action 列表 actions,执行 Task 就是按顺序执行这个列表,Action 是比 Task 更细的代码单元。Task 支持添加多个动作,Task 提供了两个方法来添加 Action:
doFirst(Closure): 在 Action 列表头部添加一个 Action;doLast(Closure): 在 Action 列表尾部添加一个 Action。task MyTaskMyTask.doFirst{ println "Action doFirst 1"}MyTask.doFirst{ println "Action doFirst 2"}MyTask.doLast{ println "Action doLast 1"}执行 MyTask 输出:Action doFirst 2Action doFirst 1Action doLast 1对于自定义 Task,还可以通过 @TaskAction 注解添加默认 Action。例如:
abstract class CustomTask extends DefaultTask { @TaskAction def greet() { println 'hello from GreetingTask' }}
5.7 跳过 Task 的执行
并不是所有 Task 都会被执行,Gradle 提供了多个方法来控制跳过 Task 的执行:
1、onlyIf{}: 闭包会在即将执行 Task 之前执行,闭包返回值决定了是否执行 Task;2、enabled 属性: Task 的 enabled 属性默认为 true,设置为 false 表示无效任务,不需要执行。剩下两种方式允许在执行 Task 的过程中中断执行:
3、Task 异常: Task 提供了两个异常,能够当 Action 执行过程中抛出以下异常,将跳过执行并继续后续的构建过程:StopActionException: 中断当前 Action,并继续当前 Task 的下一个 Action;StopExecutionException: 中断当前 Task,并继续 Task 依赖树上的下一个 Action。4、timeouts 属性: 当 Task 执行时间到达 timeouts 超时时间时,执行线程会收到一个中断信号,可以借此许控制 Task 的执行时间(前提是 Task 要响应中断信号)。
5.8 Task 依赖关系
通过建立 Task 的依赖关系可以构建完成的 Task 有向无环图:
dependsOn 强依赖: Task 通过 dependsOn 属性建立强依赖关系,可以直接通过 dependsOn 属性设置依赖列表,也可以通过 dependsOn() 方法添加一个依赖;输入输出隐式依赖: 通过建立 Task 之间的输入和输出关系,也会隐式建立依赖关系。例如 Transform Task 之间就是通过输入输出建立的依赖关系。// 通过属性设置依赖列表task task3(dependsOn: [task1, task2]) {}// 添加依赖task3.dependsOn(task1, task2)依赖关系:task3 依赖于 [task1, task2],在执行 task3 前一定会执行 task1 和 task2在某些情况下,控制两个任务的执行顺序非常有用,而不会在这些任务之间引入显式依赖关系,可以理解为弱依赖。 任务排序和任务依赖关系之间的主要区别在于,排序规则不影响将执行哪些任务,只影响任务的执行顺序。
mustRunAfter 强制顺序: 指定强制要求的任务执行顺序;shouldRunAfter 非强制顺序: 指定非强制的任务执行顺序,在两种情况下会放弃此规则:1、该规则造成环形顺序;2、并行执行并且任务的所有依赖项都已经完成。task3 mustRunAfter(task1, task2)task3 shouldRunAfter(task1, task2)依赖关系:无,在执行 task3 前不一定会执行 task1 和 task2顺序关系:[task1, task2] 优先于 task3
5.9 Finalizer Task
给一个 Task 添加 Finalizer 终结器任务后,无论 Task 执行成功还是执行失败,都会执行终结器,这对于需要在 Task 执行完毕后清理资源的情况非常有用。
// taskY 是 taskX 的终结器taskX finalizedBy taskY
6. 增量构建
6.1 什么是增量构建?
任何构建工具都会尽量避免重复执行相同工作,这一特性称为 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 会在不必要时执行。
6.2 Task 输入输出
大多数情况下,Task 需要接收一些 input 输入,并生成一些 output 输出。例如编译任务,输入是源文件,而输出是 Class 文件。Task 使用 TaskInputs 和 TaskOutputs 管理输入输出:
Task#inputs: 返回 Task 的 TaskInputs 输入管理器;Task#outputs: 返回 Task 的 TaskOutputs 输出管理器。
对于 Task 的输入输出,我们用面向对象的概念去理解是没问题的。如果我们把 Task 理解为一个函数,则 Task 的输入就是函数的参数,而 Task 的输出就是函数的返回值。在此理解的基础上,再记住 2 个关键点:
1、隐式依赖: 如果一个 Task 的输入是另一个 Task 的输出,Gradle 会推断出两者之间的强依赖关系;2、在配置阶段声明: 由于 Task 的输入输出会用于构建依赖关系,那么我们应该确保在配置阶段定义输入输出,而不是在执行阶段定义。Task 支持三种形式的输入:
1、简单值: 包括数值、字符串和任何实现 Serializable 的类;2、文件: 包括单个文件或文件目录;3、嵌套对象: 不满足以上两种条件,但其字段声明为输入。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();}
6.3 Task 输入输出校验
通过注解方式注册输入输出时,Gradle 会在配置阶段会对属性值进行检查。如果属性值不满足条件,则 Gradle 会抛出 TaskValidationException 异常。特殊情况时,如果允许输入为 null 值,可以添加 @Optional 注解表示输入可空。
@InputFile: 验证该属性值不为 null,并且关联一个文件(而不是文件夹),且该文件存在;@InputDirectory: 验证该属性值不为 null,并且关联一个文件夹(而不是文件),且该文件夹存在;@OutputDirectory: 验证该属性值不为 null,并且关联一个文件夹(而不是文件),当该文件夹不存在时会创建该文件夹。
7. 总结
到这里,Gradle 基础的部分就讲完了,下一篇文章我们来讨论 Gradle 插件。提个问题,你知道 Gradle 插件和 .gradle 文件有区别吗?关注我,带你了解更多。
参考资料
《实战 Gradle》—— [美] Benjamin Muschko 著,李建 朱本威 杨柳 译《Gradle for Android》—— Kevin Pelgrims 著,余小乐 译Groovy 参考文档 —— Groovy 官方文档Gradle 说明文档 —— Gradle 官方文档Gradle DSL 参考文档 —— Gradle 官方文档深入探索 Gradle 自动化构建技术(系列) —— jsonchao 著
版权声明:本文转载于网络,版权归作者所有,如果侵权,请联系本站编辑删除