Android Tech And Thoughts.

Gradle Leaning

Word count: 1.6kReading time: 5 min
2020/01/11 Share

gradle.png

本文主要参考温哥的Blog,作为一些自己的学习笔记,在学习的过程中,可能会有一些自己的总结,或者对原文的提炼,所以建议自行阅读 Gemiwen 大佬的 Blog,这里附上原文地址

Gemini’s story

Gradle Builds Everything - 基础概念

本文是从撰写Gradle Plugin的角度,希望把Gradle体系的一些基础结构讲明白

首先我们能应该明白,gradle的工作是把所有的构建动作管理起来—任务是否应该执行,什么时候执行,执行某个任务前先做些什么事情,某几个动作是否可以并行执行。

什么是任务

一个任务我们可以理解为把一次指定的输入,转换成想要的输出。比如 [编译] 这个动作,就是把 .java 文件编译成 .class ,或者执行 appt ,把资源文件编译成一个 resource.ap_ 文件等. 任务的基础就是这么简单,然后为了加快执行速度,gradle 增加了 UP-TO-DATE [TODO:不理解]检查(只要输入和输出的文件不发生变化,那么这个任务就不再执行) , 也增加了 incremental build 的特性(下一次的编译,并不只是把 .class 全部删除,重新编译一次这样粗暴,而是只编译变化了的几个文件)

在这种细粒度的情况下,我们对于任务执行的正确性和效率都有了保障。

『构建』的生命周期

关于Gradle的构建任务,其实网上有很多文章介绍了,无非是介绍任务的定义方式,任务的 doFirst 和 doLast ,但是很少介绍其它的元素,我们从 gradle plugin 的视角介绍一下这些概念。在一开始之前,我们需要了解一下 gradle 这个容器的一些最基础的流程 — gradle 构建生命周期

Gradle官方文章/lifecycle

Gradle 在执行的时候,会经历三个过程

  • 初始化
  • 配置 :配置阶段是一个重要阶段,我们要告诉每一个 Task,它的输入文件是什么(比如源码文件,资源文件) ,输出文件或文件夹是什么(比如编译后的 .class 文件,ap_ 等资源包放在哪个文件夹下面)等等
  • 执行 : 那么执行阶段,就是真正地执行任务的时候了

我们需要在执行的函数中,拿到在配置阶段定义的 Input ,然后产出 Output ,放在规定的目录下,或者写入指定的文件即可。

对于我们来说,理解生命周期尤为重要,如果你在 configuration 阶段去获取一个 Task 的结果,从逻辑上讲是很愚蠢的。所以你需要知道你的代码是在 “什么状态下” 执行这一步操作。

任务间的依赖

当我们知道了生命周期后,就要开始思考一个问题,比如 B 任务的一些输入依赖于 A 任务的一些输出,这时候就需要配置 B 任务依赖 A 任务,那我们如何保证这一点呢?

有一个办法,那就是对 B 任务调用显式依赖B.dependsOn(A)这样 B 一定在 A 之后执行的,B 任务中对于某个由 A 产生的文件的读取是一定能读到的。不错,它是个好办法,但问题就在于,这样的指定方式耦合度非常高,如果你需要加入一些对A产物的一些修改,然后再传给B的时候,就没有任何办法了。B同时知道了A的存在,如果我们这时候不希望由A任务提供这个文件,而是由A’来提供这个输出,在这里也做不到,所以需要换一个思路。

Gradle 提供了并使用了非常多像 Provider,Property,FileCollection 之类这样的类。看名字我们大概能知道,这些方法都提供了一个 get() 方法,获取到里面保存的实例。但是 Gradle 对于这个 get() 方法赋予了更多的意义,它可以把依赖关系放进去,当你调用get()的时候,可以检查它的依赖的任务是否已经执行完成,如果已经完成,那么再返回这个值。

1
@NonExtensible
2
public interface Provider<T> {
3
4
    /**
5
     * Returns the value of this provider if it has a value present, otherwise throws {@code java.lang.IllegalStateException}.
6
     *
7
     * @return the current value of this provider.
8
     * @throws IllegalStateException if there is no value present
9
     */
10
    T get();
11
12
    //.....
13
}

有了上面这个特性,我们定义起依赖关系就简单多了,我们把一个任务的输出文件用 Provider 包裹起来,也就是 Provider 这样的类型提供,由 Gradle 或者自行为这些 Provider 设置 dependsOn ,然后再把这些 Provider 分发给其它的 Task

另外的 Task 只要保证它只在执行阶段去调用这些 Provider 的 get 方法即可。Provider 只是一种意图,因此他们可以先把 Provider 存到 Task 实例的成员变量里,同时使用 Gradle 提供的@Input/@InputFile/@OutputFile等注解为这些 Provider 的 getter 进行标注,这样能让 Gradle 把这些值管理起来。

这样我们解决了第一个问题 —— Task 之间不在显式依赖。如果我们想实现在 Task A 和 Task B 之间做一些 Hook 的话,我们这时候要对 Provider 做一个管理,我们可以做一个全局管理器,为每一个产物集合做一个名字或者枚举的标记,然后对对应的标记定义一系列的动作,比如替换这个标记的产物,或者追加产物等,以便于后续的任务能更好的处理这里产生的产物。

这样我们解决了第一个问题 —— Task 之间不在显式依赖。如果我们想实现在 Task A 和 Task B 之间做一些 Hook 的话,我们这时候要对 Provider 做一个管理,我们可以做一个全局管理器,为每一个产物集合做一个名字或者枚举的标记,然后对对应的标记定义一系列的动作,比如替换这个标记的产物,或者追加产物等,以便于后续的任务能更好的处理这里产生的产物。

before decoupling

after decoupling

CATALOG
  1. 1. Gradle Builds Everything - 基础概念
    1. 1.1. 什么是任务
    2. 1.2. 『构建』的生命周期
    3. 1.3. 任务间的依赖