Android Gradle 学习笔记构建块基本单元掌握

Posted RikkaTheWorld

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Gradle 学习笔记构建块基本单元掌握相关的知识,希望对你有一定的参考价值。

本篇主要学习 Gradle 构建脚本的三个基本组建成:

  • project 项目
  • task 任务
  • property 属性

我将会重点介绍 Task,因为它是所有基础中的基础, project 的知识更多和实战有关(例如模块化),而 property 本身不过就是一个属性变量,没有什么东西可讲。


目录

1. Project

在 Gradle 中,一个 Project 表示一个项目,每一个 .gradle 文件都会和一个 Project 对象相关联,它最终会编译成 Project 字节码。 它用于表示一个整体的目标,例如你用一个 Project 来做 android 的自动化打包、做一个 Web 应用等等。

在运行构建时,Gradle 会实例化一个 Project ,你可以在脚本中通过 project 句柄获取该实例,我们可以调用该实例的方法来获取或配置项目的参数。它是我们构建脚本中与 Gradle 交互的主要 Api。

Project 是一个Java接口,位于:org.gradle.api.Project ,它的成员函数如下图所示:

通过上面这些函数,我们能知道 Project 有这些定义好的能力:

  • 可以在项目上创建新任务(task 函数)
  • 添加依赖和配置 (dependenciesconfigurations 函数)
  • 应用插件或其它脚本 (apply 函数)
  • 可以通过 getter / setter 来获取属性,例如 name、 description 等等。

我们可以通过 project 句柄来使用这些能力,如:

project.description = "rikka the project"
println("project的描述为:$project.description")

由于构建脚本是基于 Project ,咱们写的代码都是在 Project 的作用域中,了解 Kotlin DSL 的同学会知道,.kts 文件其实就是在一个 lambda 表达式里面编程,而这个 lambda 表达式的接收者就是 Project ,带有接收者的函数类型的特点是我们可以隐式调用接收者的函数,因此我们不用显示声明 project

description = "rikka the project"
println("project的描述为:$description")

在我们实际的项目中,一般都是多 project 构建,例如 Android 中就有一个 根Project 的 build.gralde 和至少一个 module 的 buidle.gradle。因为项目越复杂,单个 project 承载的任务会越多越臃肿,我们就越有拆分拆模块的必要。这也引申出 模块化 这是个非常重要的概念, Gradle 对于这方面的支持也已经非常强大,会在后面学习到。

2. Task

TaskProject 作用域下的执行单元,和 Project 一样,在 Gradle 中,它被赋予了一些能力。

Task 是一个 Java 接口,位于 org.gradle.api.Task,它的成员函数如下图所示:

通过这些函数,我们能知道 Task 的能力:

  • 可以依赖其他 Task( dependsOn 函数 )
  • 可以定义执行阶段初始和末尾动作 (doFirstdoLast 函数)
  • 通过 getter / setter 获取一些配置属性

2.1 任务的结果

之前的章节里,在执行任务时,我们会看到:

请注意最后一行的信息,在平日的 Android 开发中,你会留意到:在点击 build 运行项目时,控制台中会打印 Gradle 的执行信息,可能会有一些 "Task :xxxxx NO-SOURCE""Task :xxxx UP-TO-DATE" 等字样信息, 这些信息其实是 Gradle 任务执行的结果,它有好几种类型,表明任务执行情况,下面介绍这些类型和意义:

  • (没有标签) 或 EXCUTED
    • 任务已经执行动作
    • 任务没有动作,但依赖n个任务,这n个任务都被执行完成
  • UP-TO-DATE
    表示这个任务的结果已经是最新的。如果一个任务只定义了输出, 如果输出不变的话, 它就会被视为 up-to-date
    • 任务第一次被执行时产生了一个结果,这次执行后会将结果和上次进行比对,发现是一样的,说明这个任务其实没有进行任何改动(这里一般被用做增量构建)
    • 任务告诉 Gradle 没有更改其输出
    • 任务没有任何动作,有一些依赖,但是这些依赖的结果都是 “UP-TO-DATE”、“SKIPPED”、“FROM-CACHE
    • 任务没有任何动作,也没有任何依赖
  • FROM-CACHE
    任务的输出从原来的输出缓存中还原。
  • SKIPPED
    任务没有被执行
    • 该任务已经在命令行中被明确排除
    • 任务有个 onlyIf 谓词,但是该谓词返回 false,所以不执行
  • NO-SOURCE
    任务不需要执行其动作
    • 任务有定义输入和输出,但是没有输入。例如在使用 javac 命令编译 Java 时,没有输入的 .java 文件。

上面 UP-TO-DATEFROM-CACHE 看起来有点像,下面引用 What is Different? 来看下它们的区别:

Gradle有两个主要的特性:增量构建任务输出缓存

  • 增量构建
    用于执行那些自上次运行依赖没有发生更改的任务。为了实现这一点, gradle 会将执行的输入输出以“快照”的形式存储在本地目录 .gradle 文件中。当你运行一个已经执行且没有更改的任务时,这个任务会打印 “UP-TO-DATE
  • 任务输出缓存
    主要用于 CI 环境,当在 CI 运行的每个阶段时,会删除和检出本地目录,这意味着保存缓存的 .gradle 本地文件会在每个阶段消失,因此,任务每次被重新构建,增量构建都不会起作用,此时代替它的将是 任务输出缓存,它的能力是在 gradle 执行时,生成一个缓存,应用于填充本地 .gradle 目录,当构建缓存被用来重新填充本地目录时,任务被标记为 “FROM-CACHE”,一旦本地目录被重新填充,下一次执行将把任务标记为“UP-TO-DATE”而不是“FROM-CACHE”。

2.2 任务的定义

2.2.1 任务的创建

我们可以通过 tasktasks 句柄来创建任务,如下代码所示:

task("HelloWorld") 
	println("Hello World")



// 这里的用法等于 task("printRikka") 
tasks.create("printRikka") 
    println("Rikka")

在上面代码中,我们使用了 task(taskName) .. 创建了一个任务。在每次 Gradle 开启时,会先跑一遍它的配置代码。

这里可能会有些小问题:比如我不想它那么早的运行配置代码,如果我弄了很多 task,一些还是非必要的、动态的,那岂不是容易影响性能?

因此 Gradle 提供了懒加载 task,我们可以通过 tasks.register() .. 来创建一个懒任务,它在被执行时,才会去配置运行:

// 定义一个懒任务
tasks.register("printHello") 
    println("Hello")


// Kotlin 可以使用委托进行定义任务
val hello by tasks.registering 
    doLast 
        print("Hello Delegate last")
    

运行 Gradle,你会发现啥都没有打印,只有执行这些任务才会打印, Gradle 推荐我们使用 tasks.register 来创建任务以提高性能,之后的代码我也会这样去配置任务信息。

2.2.2 doFirst 和 doLast 的定义

doFirstdoLast 的作用是什么呢?

Task 可以分为两个部分:

  • 配置部分
    Gradle 会在配置阶段去跑一遍 Task, 例如设置描述、组别
  • 执行部分
    Task 有实际的功能,例如签名、删除文件等,可能会被其它任务依赖、调用才会去执行,因此不需要在配置阶段就去执行这些任务。所以 doFirstdoLast 的意义就是在执行阶段进行的代码,它们不会在配置阶段执行。

看下面代码:

task("myTask") 
    doFirst 
        println("first")
    
    doLast 
        println("last")
    
    description = "rikka the project"
    println("project的描述为:$description")

若不执行这个 task,而是跑一个简单的 gradle 命令,打印如下:

>  Configure project :
project的描述为:rikka the project

而若执行了 gradle myTask,将会打印:

> Configure project :
project的描述为:rikka the project

> Task :myTask
first
last

doFirst 和 doLast 的特性如下:

  • 通过 doFirst / doLast 来编写具体执功能的代码
  • doFirst / doLast可以指定多个。
  • 外部可以指定一个 task 的doFirst / doLast,而且这些块会比内部指定的先执行
  • 通过 doFirst / doLast, 我们能对原有 task 进行扩展而不破坏原有代码结构,如:
task("printRikka") 
    println("Rikka")


// 对已定义的任务增加额外功能
// 这里使用 tasks 来索引全局任务, 也可以使用 tasks.getByName("printRikka") 获取该任务
tasks["printRikka"].doFirst 
    println("Hello World")

2.2.3 调用任务

当我们创建了一个任务后,后面需要使用这个任务时,该如何调用它呢?

task("printHello")      // 创建一个任务
task<Copy>("copyTask")  // 创建一个复制文件的任务

// 使用 Kotlin 代理来访问

val printHello by tasks.getting    // 指定任务名
println(printHello.name)

val copyTask by tasks.getting(Copy::class)  // 指定任务名和类型
println(copyTask.destinationDir)

也可以通过 tasks 来获取:

tasks.register("printHello")
tasks.register<Copy>("copyTask")

println(tasks["printHello"].name)
println(tasks.named("printHello").get().name)

println(tasks.getByName<Copy>("copyTask").destinationDir)
println(tasks.named<Copy>("copyTask").get().destinationDir)

如果这个任务被定义在别的项目中,我们需要使用相对路径 / 绝对路径的方式来访问:

project(":projectA") 
    tasks.register("printHello")  // 别的项目有一个任务


tasks.register("printHello")  // 本项目中有一个同名任务

println(tasks.getByPath("printHello").path)  // 打印 :printHello
println(tasks.getByPath(":printHello").path)  // 打印 :printHello
println(tasks.getByPath("projectA:printHello").path)  // 打印 :projectA:printHello
println(tasks.getByPath(":projectA:printHello").path) // 打印 :projectA:printHello

2.2.4 任务配置

Gradle 内置了一些已有的任务类型,方便我们复用。例如复制文件的任务 Copy,我们通过配置复制的目标文件和目的地,就能轻松创建一个复制任务:

val myCopy = tasks.named<Copy>("myCopy")   // 创建一个 Copy 任务,名称为 myCopy

myCopy   // 通过 DSL 进行配置
    from("resources")  // 使用 api 来配置数据源
    into("target")     // 使用 api 配置目的地
    include("**/*.txt", "**/*.xml", "**/*.properties")   // 配置 include 信息

这种写法比较冗余,因为它的创建代码和执行代码分离了,不太符合我们的习惯,下面这种写法更加易读:

val myCopy by tasks.existing(Copy::class)   // 创建和执行写在一起, 也可以用 named、getByName
    from("resources")
    into("target")

myCopy  include(...)   // 外部配置

// 也可以这样写:

tasks.register<Copy>("myCopy ") 
   from("resources")
   into("target")
   include(...)

2.2.5 参数传递

与有参数可以配置的Task不同,一些任务是需要入参来运行的,之前创建任务的方式不能满足这种条件。为了达到这个目的,我们必须使用 @javax.inject.Inject 注解来创建可入参的任务类:

// 创建一种 Task 类型,它可以接收一个 String 和 Int 作为参数
open class CustomTask @Inject constructor(
    private val message: String,
    private val number: Int
) : DefaultTask()

接下来,就像使用 Copy 类型任务一样,去使用这个 CustomTask

tasks.register<CustomTask>("customTask", "printHello", 11)

// 下面这种方式会麻烦一点:
task("customTask", "type" to CustomTask::class.java, "constructorArgs" to listOf("printHello", 11))

2.2.6 添加依赖项

我们在前面已经提到过任务的依赖了,通过 dependsOn() 即可配置依赖任务,跨项目的任务依赖,也可以通过路径名的方式来添加:

project("projectA") 
    tasks.register("taskA") 
        dependsOn(":projectB:taskB")
        doLast 
            println("taskA")
        
    


project("projectB") 
    tasks.register("taskB") 
        doLast 
            println("taskB")
        
    


// 执行 taskA,将会打印:
taskB
taskA

除了通过任务名称进行依赖,也可以直接使用 Task 对象本身:

val taskA by tasks.registering 
    doLast 
        println("taskA")
    


val taskB by tasks.registering 
    doLast 
        println("taskB")
    


taskA 
    dependsOn(taskB)


// 执行 taskA, 打印:
taskB
taskA

假如任务需要依赖多个其它任务,一次性写多个 dependsOn 可能会让代码有些臃肿,我们可以使用 Gradle Provider 提供的特性来优化代码:

val taskA by tasks.registering 
    doLast 
        println("taskA")
    


// 使用 Provider 来拿到需要依赖的任务, 这里使用 filter 过滤器
taskA 
    dependsOn(provider 
        tasks.filter  task -> task.name.startsWith("lib") 
    )


tasks.register("lib1") 
    doLast 
        println("lib1")
    


tasks.register("lib2") 
    doLast 
        println("lib2")
    


tasks.register("notALib") 
    doLast 
        println("notALib")
    


// 执行 gradle taskA, 打印:
lib1,
lib2,
taskA

2.2.7 跳过任务

Gradle 提供了一些方法来跳过任务,它有三种通用方法,下面来介绍它们。

2.2.7.1 使用 onlyIf

通过配置任务的 onlyIf .. ,若 lambda 内部返回 true,该任务才会被执行:

val hello by tasks.registering 
    doLast 
        println("hello world")
    


hello 
    onlyIf  !project.hasProperty("skipHello")


// 执行 gradle hello -PskipHello 输出:
Task :hello SKIPPED

2.2.7.2 使用 StopExecutionException

任务通过在执行过程中主动调用该异常,并不会终止整个 Gradle 构建,而将跳过该动作的执行,然后 Gradle 将继续执行下个任务:

val compile by tasks.registering 
    doLast 
        println("doing compile")
    


compile 
    doFirst 
        // 这里可能会遇到一些问题,不能继续执行该任务了
        if (true) 
            throw StopExecutionException()
        
    

tasks.register("myTask") 
    dependsOn(compile)
    doLast 
        println("do myTask")
    


// 执行 gradle myTask ,输出:
do myTask

2.2.7.3 启动和禁止任务

每个任务都有个 enable配置,默认为 true。反之将不会执行任何动作,被调用时会被标记为 SKIPPED

val disableMe by tasks.registering 
    doLast 
        println("This should not be printed")
    

disableMe 
    enabled = false


// 执行 gradle disableMe 输出:
Task :disableMe SKIPPED

2.3 增量构建

增量构建是 Gradle 最重要的功能之一。在上面我们有介绍到,如果任务执行的结果是 UP-TO-DATE,那么说明该任务的输入输出没有更改,所以没有重新构建,从而节省了时间。那么增量构建是如何进行的呢?下面来简单阐述下原理。

正常情况下,任务存在一些输入,和一些输出,以下面编译 Java 为例子,输入是 Java 源文件和一些配置信息(如调试信息),输出是字节码文件:

上图中有三个指向 Task 的箭头,但只有两个箭头被认为是输入,这是因为:输入的特征是能够决定输出

  • 源文件:可以决定生成的字节码,所以它肯定是输入
  • 版本号:同上,因为不同的 JDK 版本,源文件会生成不同的字节码
  • Fork:是一些不会影响输出内容的参数,例如 memoryMaxiumSize 属性,它只是决定了编译环境的最大可用内存,所以对于 Gradle 来说,它只是一个任务的内部属性

Gradle 会检查当次执行任务的输入,和上次任务的输入(缓存在本地 .gradle 文件中),如果它们是相同的,则 Gradle 会任务该任务已是最新的,将会输出 UP-TO-DATE,并跳过执行。

具体的增量构建将会在之后的章节中详细讲解。

2.4 生命周期任务(Lifecycle tasks)

生命周期任务是不能自行工作的任务,它们通常没有任何动作,它们将一些东西进行了抽象:

  • 工作流程步骤(例如: checkbuild
  • 可构建的事务 (例如:创建一个用于调试的 32位 的可执行文件 debug32Executable
  • 一个父任务,用于组合一系列子任务(例如: 运行所有编译任务的 comileAll

插件都定义了一些标准的生命周期任务,例如 buildassemblecheck,所有主流语言都有相应的插件,它们都具有相同的生命周期任务。除非你去指定执行这些任务,否则它们的执行将会取决其任务依赖。

3. Property

在 Gradle 中,属性有两种,一种是 Project 和 Task 提供 getter / setter 来获取和配置的属性,这些我们在上面的 Task 中有学到,很多任务会自己定义配置属性,另外一种则是自定义属性。

对于自定义属性,我们可以轻松的使用 val、var 创建一个本作用域使用的属性,也就是局部变量:

// 定义一个变量
val dest = "dest"

// 这里表示一个复制任务,将 source 目标复制到 dest
tasks.register<Copy>("copy") 
    from("source")
    into(dest)

有局部变量,也就有全局变量,在 Gradle 中,全局变量被称为 “额外属性”、“外部属性”,可以通过 extra 定义,也可以使用 Kotlin 的委托:

// 使用委托定义一个外部变量
val myVersion by extra("1.1.0.RELEASE")
val myEmail by extra  "build@rikka.org" 

// 使用 extra 为 project 作用域定义属性
extra["myProp"] = "myValue"
extra.set("myProp1", "myValue1")

tasks.register("printProperties") 
    doLast 
        println(myVersion)
        println(myEmail)
        println(project.extra["myProp"])
        println(project.extra["myProp1"])
        println(extra["myProp2"])   // 没有定义,执行到这里会报错
    

调用 priontProerties 任务:

参考

以上是关于Android Gradle 学习笔记构建块基本单元掌握的主要内容,如果未能解决你的问题,请参考以下文章

Android Gradle 学习笔记概述

Gradle笔记——构建基础

Gradle学习笔记

Android Gradle 插件组件化中的 Gradle 构建脚本实现 ⑤ ( 优化 Gradle 构建脚本 | 构建脚本结构 | 闭包定义及用法 | 依赖配置 | android 块配置 )

Android开发:《Gradle Recipes for Android》阅读笔记(翻译)4.1——编写自己的任务

Gradle构建流程-Android