2

提到 Gradle,熟悉 Android 的人都不会陌生,在我们开始把 Android Studio 这个 IDE 扶正的时候,gradle 就彻底进入了我们的视野。但是大多数人对于 gradle 执行构建和构建流程都比较陌生,本文从编写 Gradle Plugin 的角度,希望把 Gradle 体系的一些基础结构能讲明白。

首先我们明白,gradle 的工作是把所有的构建动作管理起来 —— 任务是否应该执行,什么时候执行,执行某个任务前先做一些什么事情,某几个动作是否可以并行执行。
对于 gradle plugin 的编写就是为了帮我们完成这些事情。如果你单单从任务的纬度去看这个问题的话,又会想到如果 B 需要 A 的产物的话,是否需要把 A 和 B 进行一些耦合。显然,对于任务间的解耦,Gradle 也做了。

什么是任务

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

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

「构建」的生命周期

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

官方文档: https://docs.gradle.org/curre...

如文档所示,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()的时候,可以检查它的依赖的任务是否已经执行完成,如果已经完成,那么再返回这个值。

@NonExtensible
public interface Provider<T> {

    /**
     * Returns the value of this provider if it has a value present, otherwise throws {@code java.lang.IllegalStateException}.
     *
     * @return the current value of this provider.
     * @throws IllegalStateException if there is no value present
     */
    T get();

    //.....
}

有了上面这个特性,我们定义起依赖关系就简单多了,我们把一个任务的输出文件用 Provider 包裹起来,也就是Provider<File>这样的类型提供,由 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 做一个管理,我们可以做一个全局管理器,为每一个产物集合做一个名字或者枚举的标记,然后对对应的标记定义一系列的动作,比如替换这个标记的产物,或者追加产物等,以便于后续的任务能更好的处理这里产生的产物。

这张图是原来的显式依赖方式
显式依赖方式

解耦后的方式是
隐式依赖方式

这样任务和任务之间就这么联系在了一起,当我们执行一条熟悉的命令:

./gradlew assembleDebug

它会把依赖产物的所有 task 全部执行一遍,事实上,assembleDebug 这个任务根本不知道自己依赖了哪些具体的任务,它只知道自己“需要”什么,产出什么(apk)。

举例

上面讲了任务依赖相关的理论知识,我们来举一个具体的例子,就以assembleDebug为例。

我们把事情说的简单点,比如assembleDebug的任务是把所有已经处理好的 dex,resources,assets 打包成一个 apk,那么这个 input 就是前面提到的三个,output 是 apk。我们在assembleDebug这个 Task 里面会看到如下的东西(伪代码):

class AssembleDebugTask {
    private Provider<File> dexInput;
    private Provider<File> resourcesInput;
    private Provider<File> assetsInput;

    private Provider<File> outputAPK;    

    @InputFile
    public Provider<File> getDexInput() {
        return dexInput;
    }

    @InputFile
    public Provider<File> getResourcesInput() {
        return resourcesInput;
    }

    @InputFile
    public Provider<File> getAssetsInput() {
        return assetsInput;
    }

    @OutputFile
    public Provider<File> getOutputAPK() {
        return outputAPK;
    }

}

以上是对产物的定义,那么在执行任务的过程中,会有这样的逻辑:

public void doTaskAction() {
    File dexInput = this.dexInput.get();
}

在这一步的过程中,Gradle 会去检查这个 Provider 的来源,有没有builtBy属性,如果有的话,会先执行buildBy的 Task,比如我们知道Dex的文件一定来源于产生 Dex 的任务,那么如果我们定义这个任务叫DexTask的话,就会先执行DexTask这个任务,才会继续执行assembleDebug了。

事实上为了加快效率,标记了@Input之类的注解的属性,gradle 在检查任务的时候,会提前去执行相关的依赖,因为在这个过程中,它可以动用并发的方式,并行执行几个任务,比如我们这依赖了三个输入,那么可以并行执行这三个任务,等到都执行完了,再去执行assemble的任务,这时候调用get就能直接返回值了。

欢迎关注我的公众号「TalkWithMobile」
公众号


Gemini
7k 声望1.5k 粉丝

一个没有文化的诗人