从宏观的角度看 Gradle 的工作过程

2,056 阅读7分钟

本文默认读者使用 macOS


以我自己为例,刚开始接触 Android 开发的时候,只是对 Java 开发有一些了解,对于 Android 开发的整个生态和技术栈只有一个模糊的认知。 缺乏对 Gradle 的认知,在初级开发阶段可能勉强可以应付,只需要知道如何添加第三方依赖,如何调整 android block 中的配置基本上就够了,但是随着项目结构越来越复杂,默认提供的构建过程渐渐不能满足开发的需求,此时就要求开发者对 Gradle 的构建过程和原理有更深入的了解,便于自定义个性化的构建过程。

在阅读完 《Gradle In Action》后,我发现 Android 工程的构建过程似乎不再那么神秘了。 本文希望能从宏观的角度带刚接触 Android 开发不久的同学认识一下 Gradle,如果对 Gradle 工作原理感兴趣,希望能够更加深入了解,建议阅读 《Gradle In Action》 这本书。

背景

随着项目规模增大,软件工程师需要考虑的事情会越来越多。成功构建并运行一个项目不再像单文件的 HelloWorld 一样简单。随着持续集成思想的普及,一次成功的构建可能分为 checkStyle,Lint,编译,单元测试,集成测试,代码裁剪,代码混淆,打包部署等多个步骤。如果项目中引用了第三方 lib,那么第三方 lib 会有版本迭代,甚至多个第三方 lib 可能又依赖了不同版本的同一个第三方 lib,造成依赖版本冲突,事情会越来越复杂。我们需要使每一个 Commit 总是能构建出完全相同的结果,Git 对于二进制文件的版本管理又不是那么得心应手,手动构建常常会引入人为变数导致构建出错。所以构建过程自动化迫在眉睫。

常见的 Java 构建工具

  • Ant (Anothre Neat Tool) 2000年
    • 使用 XML 描述构建的步骤
    • 只负责构建步骤管理,如果要添加依赖管理的功能,还需要引入 Ivy
  • Maven 2004年
    • convention over configuration 的思想,无需配置或者仅需少量配置即可开始构建
    • 和 Ant 对比增加了依赖库管理
  • Gradle 2007年
    • 使用 Groovy DSL 替代繁琐的 XML
    • 支持增量构建
    • 项目结构更加灵活

Google 基于 Gradle 通过 Android Gradle Plugin 提供了自动化构建的工具,对开发者隐藏了大量的繁琐的构建过程,暴露一些可被开发者配置的属性,大大的简化了 Android 项目管理的复杂度的同时又不失灵活性。

在这里列举的构建工具不止可以用来构建 Java 相关的项目。只要能表达出构建步骤,就可以使用这些工具来进行项目构建。比如,你可以使用 Gradle 来构建一个 iOS 的项目。

Gradle Wrapper:

The Wrapper is a script that invokes a declared version of Gradle, downloading it beforehand if necessary. As a result, developers can get up and running with a Gradle project quickly without having to follow manual installation processes saving your company time and money.

docs.gradle.org/current/use…

构建工具也是需要版本迭代的,一个大的版本迭代可能不会提供向前的兼容性,也就是说,在 A 机器上和 B 机器上装了两个不同版本的 Gradle,结果可能导致同一个项目,在 A 的机器上可以成功构建,而在 B 的机器上会构建失败。 为了避免这个问题,保证每个 Commit 总能构建出完全相同的结果。Gradle 提供了 Gradle Wrapper,通过 Wrapper 运行 Gradle Task 的时候,会先检查 gradle-wrapper.properties 中指定的位置下,指定版本的 Gradle 是否安装,如果已经安装,则将该 Gradle Task 交给 Gradle 处理。如果没有安装,则先下载安装指定版本的 Gradle,然后再将 Gradle Task 交给 Gradle 处理。 gradlew 是一个 script,是 Gradle Wrapper 的入口,Windows 下是 gradlew.bat。 gradle-wrapper.jar 提供了 Gradlew Wrapper 的核心功能。

目录结构如下图:

图片

如下图所示是一个典型的使用 Gradle 进行构建的 Android 工程。 工程中包含两个 Project:

  1. TutorialAndroid -- RootProject
  2. app -- SubProject

图片
可以使用如下命令查看工程中的 Project

gradlew projects

gradlew 是入口 Script, projects 实际上是 Gradle 一个内置的 Task。 关于 Task 的概念,下面再解释。 运行上面的命令,结果如下图所示,可以看到,一般我们开发时修改 **app **只是一个子项目,RootProject 实际上是 app 的上级目录中的 TutorialAndroid。

图片

构建过程

Gradle 的构建过程分为以下几个阶段: initialization -> configuration -> execution

  1. initialization phase
    • Gradle 使用 Project 对象来表示项目,在 initialization 阶段,Gradle 会为每个参与本次构建的项目创建一个 Project 对象。
    • 因为 Gradle 支持多项目构建,所以在初始化阶段的时候,需要判断哪些项目需要参与本次构建。
    • Gradle 可以从 Project 的根目录开始构建,也可以从任意包含 build file 的子文件架开始构建。无论从哪里开始构建,Gradle 都需要知道有哪些 Project 需要参与构建,Root Project 的 settings.gradle 中声明了需要参与构建的 Project 的信息。所以 Gradle 在这个阶段做的事情,就是从当前目录开始,逐级向上搜索 settings.gradle ,如果找到了,就按照 settings.gradle 中声明的信息设置本次构建,如果最终没有找到,那么就默认只有当前所在的 Project 需要参与本次构建。
  2. configuration phase

    A Task represents a single atomic piece of work for a build, such as compiling classes or generating javadoc.

    A Task is made up of a sequence of Action objects. When the task is executed, each of the actions is executed in turn, by calling Action.execute(T). You can add actions to a task by calling Task.doFirst(org.gradle.api.Action) or Task.doLast(org.gradle.api.Action).

    docs.gradle.org/current/dsl…

    • Task 属于 Project 对象。可以在 build.gradle 文件中简单定义 Task
     // 定义好 Task 之后,就可以通过 `gradlew simpleTask` 来运行指定的 Task
     task simpleTask {
         doLast {
         println "This is a simple task."
         }
     }
    
    • 项目构建过程分为很多步骤,在 Gradle 中用 Task 来表示这些步骤,Task 之间可能有依赖关系,例如:必须先执行完 compile Task,才能执行 unitTest Task。在 configuration 阶段,Gradle 会分析 Task 之间的依赖关系,配置初始化阶段创建的 Project 对象。

    Gradle determines the subset of the tasks, created and configured during the configuration phase, to be executed. The subset is determined by the task name arguments passed to the gradle command and the current directory.

    docs.gradle.org/current/use…

    • 当一个 Project 的 Task 越来越复杂,或者多个项目都需要共用同一个 Task 的时候,为了提高代码复用性,可以编写 Plugin 将创建 Task 等逻辑封装起来。

      图片
      build.gradle 中,如图所示就是在使用封装好的 Plugin。

    • 提高了代码复用性的同时,还需要提供足够的灵活性。Plugin 可以通过 Extension 暴露一些可配置的属性。这里先不讲,超纲了。

  3. execution phase
    • 根据上一步计算出的任务执行顺序去执行需要执行的 Tasks。

以上就是 Gradle 的工作过程。

Tricks

  1. 使用 Proxy

    在国内特殊的网络环境,可以通过设置 Proxy 或 Repo Mirror 的方式来提高下载依赖的 Library 的速度。

    阿里提供的镜像 maven.aliyun.com/mvn/view Gradle 使用 Java 的 Networking Properties 读取 Proxy 参数。可供设置的参数参考以下文档。

  2. 注意你的电脑中运行了多少 Gradle Daemon

    Gradle 提供了 Daemon 机制来提高构建速度,但是 Gradle Daemon 的复用是有条件的。 如果恰巧给 Gradle Daemon 设置了一个比较大的 maximum heap size, 可能在开发的过程中,多个 Daemon 会占用过多的内存,影响电脑运行速度。除了前面给出的条件,还有两点是之前开发过程中遇到过的:

    • 没有正确使用 Gradle 提供的 wrapper Task 去升级 Gradle 版本,导致在使用 Gradle Wrapper Script 运行任务时,判断 Gradle 版本的函数不兼容,启用多个 Daemon。这个问题的表现方式一般发现同一个版本的 Gradle 被 Gradle Wrapper 重复下载。
    • IDE 提供了 Build in JRE,导致在 IDE 中运行的 Gradle 和在 Terminal 下运行的 Gradle 虽然版本相同,但是无法复用。可以统一两者使用的 JRE。比如在 Android Studio 中打开 Project Structure,在 SDK Location Tab 下设置 JDK Location,使其和 Terminal 中使用的 Java 路径统一。

参考资料

《Gradle In Action》 -- Benjamin Muschko

Gradle 最佳实践

构建工具的进化:ant, maven, gradle

@Eric