十分钟搞定 Gradle

10,585 阅读7分钟

项目经验,如需转载,请注明作者:Yuloran (t.cn/EGU6c76)

前言

学习过程中,什么阶段最痛苦?大概是某个知识点的碎片信息学习了很多却仍然无法窥其门径,也就是似懂非懂的时候。对于 Gradle,笔者之前就是这种状态。在亲手完成了一个需求后,发现 Gradle 也不过如此。

由于笔者做需求时采用的是倒扒皮的方式,即先 google 搜索如何解决问题,再阅读官方 User Guide,最后总结反思,所以用了半天的时间,还踩了一些坑。如果按照本文介绍,按部就班地学习,大概十分钟就够了。所谓一通则百通,窥其门径后,若有其它需求,直接查阅 API 即可。

案例

笔者是做安卓整机开发的,目前接手了一个新项目,其 APP 分为两个版本,一个是系统预置(private),一个供其它品牌手机安装使用(public)。其中 public apk 需要打包到 private apk 的 assets 目录下,以在 private apk 上实现扫码安装 public apk 的功能。两个版本的代码目前是手动维护,很不方便。笔者便想通过创建自定义的 Task,让 Gradle 来自动构建。

问题

  • 如何创建 private、public 两个 build variants(构建变体)?
  • 如何配置 public 版本在 private 版本之前构建(因为 private 版本依赖 public 版本生成的 apk)?
  • public 版本构建完成后,如何自动复制其生成的 apk 到 private 版本的 assets 目录下?

解决方案

  • 关于构建变体,其实就是一次编译,输出多个版本的 apk,具体内容请参考官方文档中文版《配置构建变体》
  • 两个构建变体,说明对应两个 assemble task,那么只要获获取到这两个 task 对象,然后设置其依赖关系即可
  • Gradle 文件支持 groovy 编写,groovy 又是基于 java 的,所以即使不熟悉 groovy 的语法,也可以用 java 写出来。不过对于复制这种操作,Gradle 有现成的 API

如何编写

方案很清晰:assemblePublicApp -> deleteOldPublicApp -> signNewPublicApp -> copyNewPublicApp -> assemblePrivateApp

但是代码怎么写呢?笔者一时间感到无从下手。比如如何获取两个构建变体对应的 assemble task?如何创建一个 copy task?又如何在执行 copy task 之前先执行 delete task(删除 assets 目录下的旧 apk) 以及 sign task(签名 public apk)?

笔者一顿 google 搜索之后解决了这些问题,不过也踩了一个坑,就是自定义 task 内的代码执行时机不对。比如 deleteOldPublicApk task 中的日志,总是在执行 gradle assemble 命令之后立即输出,而不是在 assemblePublicApp task 之后输出:

File -> Demo/app/build.gradle

android {
    ...
}

task deleteOldPublicApk(type: Delete) {
    println("-----------> delete the old pubic apk begin") // 注意:这么写代码会在配置阶段立即执行
    delete 'src/privateApp/assets/Public.apk' // delete 方法继承自 Delete task,所以是一个 Action,在执行阶段才会被执行
    println("-----------> delete the old pubic apk end") // 注意:这么写代码会在配置阶段立即执行
}

task signNewPublicApp() {
    doFirst {
        println 'sign the new public app' // 写在 doFirst 或者 doLast 中,才会在执行阶段被执行,具体见下文
    }
}

task copyNewPublicApp() {
    doLast {
        println 'copy the new public app'
    }
}

afterEvaluate {
    def assemblePublic = tasks.getByName('assemblePublicAppRelease')
    deleteOldPublicApk.dependsOn(assemblePublic)

    copyNewPublicApp.dependsOn(deleteOldPublicApk, signNewPublicApp)

    def assemblePrivate = tasks.getByName('assemblePrivateApp')
    assemblePrivate.dependsOn(copyNewPublicApp)
}

dependencies {
    ...
}

如上所示的 deleteOldPublicApk task,只要在 terminal 中 输入 gradlew assemble 必然会首先打印:

-----------> delete the old pubic apk begin
-----------> delete the old pubic apk end

相信很多不熟悉 Gradle 的人都会犯这样的错误,stackoverflow 上有人也发出了同样的疑问 Why is my Gradle task always running?

后来笔者阅读了 Gradle 的官方文档 《Build Lifecycle》,恍然大悟,应该这么写:

task deleteOldPublicApk(type: Delete) {
    doFirst {
        println("-----------> delete the old pubic apk begin")
    }
    delete 'src/privateApp/assets/Public.apk'
    doLast {
        println("-----------> delete the old pubic apk old")
    }
}

痛定思痛,笔者决定将 Gradle 的入门在此做一个总结。

入门

Gradle 的入门其实很简单,不需要深入学习 Groovy(随用随查),也不用记 Gradle 的 API(随用随查)。只需要了解几个核心概念(构建模型、构建的生命周期、Project、Task、TaskContainer),就能做到一通百通了。

构建模型的核心

左边是构建模型的抽象,右边是一个 java 工程的具体实现。Gradle 的核心就是左边的抽象模型(有向无环图),也就是说一个完整的构建过程,其实就是一系列 Task 的有序执行。

构建生命周期

注意,这一小节尤为重要,特别是配置阶段与执行阶段的区别,一定要分清楚。

三个构建阶段

  1. Initialization:配置构建环境以及有哪些 Project 会参与构建(解析 settings.build)
  2. Configuration:生成参与构建的 Task 的有向无环图以及执行属于配置阶段的代码(解析 build.gradle)
  3. Execution:按序执行所有 Task

示例

File-> settings.gradle

println 'This is executed during the initialization phase.' // settings.gradle 中的代码在初始化阶段执行

File->Demo/app/build.gradle

println 'This is executed during the configuration phase.' // 在配置阶段执行

// 普通的自定义 Task
task testBoth {
	doFirst {
	  println 'This is executed first during the execution phase.' // doFirst 中的代码在执行阶段执行
	}
	doLast {
	  println 'This is executed last during the execution phase.' // doLast 中的代码在执行阶段执行
	}
	println 'This is executed during the configuration phase as well.' // 非 doFirst 或者 doLast 中的代码,在配置阶段执行
}

// 继承自 Copy 的 TasK
task copyPublicApk(type: Copy) {
    doFirst {
        println("-----------> copy the new pubic apk begin")
    }
    // from, into, rename 都继承自 Copy,所以即使直接写也是在执行阶段执行
    from 'build/outputs/apk/app-publicApp-release.apk'
    into file('src/privateApp/assets')
    rename { String fileName ->
        fileName = "Public.apk"
    }
    doLast {
        println("-----------> copy the new pubic apk end")
    }
}

Project

一个 build.gradle 对应一个 Project 对象,在 gradle 文件中可通过 project 属性访问该对象。而 rootProject 属性代表的是根 Project 对象,即项目根目录下的 build.gradle 文件。

Project 由一系列的 task 组成,你可以自定义 task,也可以继承已有的 task:

Project 还有自己的属性和方法:

Task types 以及 Project 的属性和方法都可以在 Groovy DSL Reference 中查到。

Task

在 gradle 文件中,我们一般使用 task 关键字来定义一个 task,通过 task 的名字就可以直接访问该 task 对象:

File -> Demo/app/build.gradle

task customTask() {
    doLast {
        println 'hello, this is a custom task'    
    }
}

如何查找一个 task 呢?通过 TaskContainer 对象,在 gradle 文件中通过 tasks 属性来访问该对象:

File -> Demo/app/build.gradle

afterEvaluate {
    def aTask = tasks.getByName('assembleDebug')
    println "aTask name is ${aTask.name}"
    aTask.dependsOn(customTask)
}

如上所示,我们获取到了 assembleDebug 这个 Task 的实例,并设置它依赖之前定义的 customTask,所以执行 assembleDebug 时就会先执行 customTask。

TaskContainer 还有很多查找 task 的方法,具体可以查询 Task Container

Gradle API 查阅指导

了解了构建模型及三大阶段,接下来就是如何查阅 API 手册了。因为 Android Studio 对 Gradle 文件的编写支持很不友好,笔者经常会出现代码没有智能提示、无法自动补全、无法代码跳转等问题,而且语法高亮也是弱的可怜。所以,必须掌握手动查阅 Gradle API 的方法。

不过现在 Gradle 文件也可以使用 kotlin 编写,语法清晰,可读性好,而且支持语法高亮、代码补全、代码跳转等。感兴趣的可以参考官方迁移教程《Migrating build logic from Groovy to Kotlin》

离线查看

Gradle 网站现在也可以正常访问了,不过 Android Studio 在下载 Gradle 插件时,已经自动将用户指南、DSL参考、API参考下载到本地了:

  • dsl:里面的内容跟 javadoc 差不多,不过是经过分类的,交互体检比 API 文档要好,主要关注核心类型里的 Project、Task 和 TaskType,具体关注里面的属性和方法,以及继承的属性和方法,用到什么就去查什么
  • javadoc:java api 文档,可以查看类的继承以及实现情况,快速索引
  • userguide:用户指南,比如 build lifecycle 的介绍,不过 html 内部的链接点击无法跳转,还好目录下有个带书签的 pdf 版

在线文档

离线文档不一定是最新的,有需要时可以查看在线文档

示例

下面这段配置大家应该都见过,我们现在想搞清楚里面的 main 是什么意思:

    sourceSets {
        main {
            manifest.srcFile 'AndroidManifest.xml'
            ...
        }
    }

直接到离线的 javadoc 中查找 SourceSet:

显然 main 是一个 SourceSet 对象,名字为:

而 sourceSets 则是一个 SourceContainer 对象,组织并管理一系列的 SourceSet 对象。

Groovy API 查阅指导

对于 Android 开发者来说,学习 Groovy 主要是为了阅读别人写的 build.gradle 文件是什么意思,因为 Groovy 是基于 java 的,所以其实完全可以使用 java 语法,只是不够简洁而已。

笔者认为 Groovy 语法最蛋疼的地方就是函数调用的圆括号可以省略,而属性赋值的 = 也可以省略,这很容易导致属性赋值与函数调用傻傻分不清楚,比如:

def aMethod(String x, String y) {
    println(x + y)
}

android {
    aMethod 'groovy', '函数调用的圆括号可以省略'
    ...
    println "project desp is: $description"
    // description 是 Project 对象的属性之一,此处将其重新赋值,且省略了 '='
    description 'The Gradle Groovy DSL allows to omit the = assignment operator when assigning properties'
    println "project desp is: $description"
}

dependencies {
    ...
}

在 terminal 中输入 gradlew assemble 将会输出

groovy函数调用的圆括号可以省略
project desp is: null
project desp is: The Gradle Groovy DSL allows to omit the = assignment operator when assigning properties

你看这个 aMethod 调用,像不像属性赋值?你看这个属性赋值,像不像函数调用?

以下来自官方迁移至 Kotlin 编写 Gradle 文件的吐槽:

As a first migration step, it is recommended to prepare your Groovy build scripts by

  • unifying quotes using double quotes,
  • disambiguating function invocations and property assignments (using respectively parentheses and assignment operator).

The latter is a bit more involved as it may not be trivial to distinguish function invocations and property assignments in a Groovy script. A good strategy is to make all ambiguous statements property assignments first and then fix the build by turning the failing ones to function invocations.

建议按照以下章节顺序,快速学习并入门 Groovy

结语

可以说 Groovy 所允许的各种省略是导致 Gradle 难以学习的罪魁祸首,虽然代码简洁了,不过可读性却差了很多。不过 Groovy 中的很多语法还是很通用的,比如方法的具名参数、参数默认值以及字符串内插等,这在 kotlin 中也有对应的语法,就是写法有些许差异而已。

所谓难而不会,会而不难,希望看完本文,各位都能有一种 Gradle 也不过如此的感觉。

上文所述皆为 Gradle 公共 API,作为 Android 开发者还需了解 Android 专属的 API:

Android Plugin DSL Reference