从编程的角度理解 Gradle

刚开始接触 Gradle 时,一头雾水。碰到不会的地方,搜索相关解决方案然后依样画葫芦。至于为什么这么做,这么做的原理是什么,完全不清楚。后来看了 Gradle 详解,觉得作者说的十分有道理,得从编程的角度而不是脚本的角度来看 Gradle。下面小结下我在这个过程中的一些收获。

从 apply 说起

Android 项目的 build.gradle 常有这么一句,apply plugin: 'com.android.application'
它常常给我一种强烈的错觉,以为 apply plugin 是一部分,'com.android.application' 是另一个部分。
而这得归因于 Groovy 调用方法的时候是可以省略括号的。拿最简单的 println 来说,你既可以写 println(3),也可以使用 println 3。如果加上省略的括号,这行代码就变成了 apply(plugin: 'com.android.application')。虽然降低了可读性,但从编程的角度来看更清晰了。
但是问题又来了,plugin: 'com.android.application' 又是个什么鬼?这就得提到 Groovy 的 Named arguments 机制了。下面拿代码说明。

1
2
3
4
5
6
def foo(Map args) {
println "${args.name}: ${args.age}"
}

// 调用方法
foo(name: 'Marie', age: 1)

调用 foo 时,name: 'Marie', age: 1 会被解析成 Map,作为 foo 的参数。
可见 plugin: 'com.android.application' 实质上是一个 Map。顺带说下在 Groovy 里,Map 的 key 默认是 String 类型,所以 plugin 两边可以不用加引号。所以 apply 实质上是以 Map 为参数的方法,可以查看 PluginAware 加以验证。
一行简单的方法调用就有这么多细节,那都是因为 Groovy 的灵活性。大家都知道 Gradle 建立在 Groovy 之上,而 Groovy 相比于 Java 灵活太多,所以想从编程的角度来理解 Gradle,对 Groovy 的熟悉度有一定的要求。建议多读读 Groovy Doc 加深对 Groovy 的学习。

接下来提高难度,如何理解下面这段代码。

1
2
3
4
5
6
7
8
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.0.0'
}
}

首先把省略的括号都加上。

1
2
3
4
5
6
7
8
buildscript({
repositories({
jcenter()
})
dependencies({
classpath 'com.android.tools.build:gradle:2.0.0'
})
})

buildscriptrepositoriesdependencies 其实都是接收 Closure 为参数的方法。调用 buildscript传入的闭包里调用了 repositoriesdependencies 方法。
了解了这些都是方法调用之后,又有个问题冒出来了,这些方法是哪里来的呢?
答案就是 Project。当 build.gradle 执行时它会配置 Project 实例并将其设为 Delegate 对象,即它的语句块都会被委托给 Project 的实例。我们继续查看 Project Doc,会发现 buildscriptScriptHandler 委托了,接着查看 ScriptHandler Doc 会找到 repositoriesdependencies 方法。
由于 Delegate 在 Gradle 中频繁使用,这里再多说几句。简单来说,Closure 的 Delegate 机制可以使我们将一个闭包中的执行代码的作用对象设置成任意其他对象。下面是来自 Setting a Closure’s Delegate 的一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Simple class.
class Post {
int count
def info() { "This is Groovy Goodness post #$count!" }
}

// Script variable and method.
count = 0
def info() {
"Count value is $count."
}

// Closure to increment a count variable and invoke a info() method.
def printInfo = {
count++
info()
}

// Delegate is by default set to owner, so the script in this case.
assert "Count value is 1."== printInfo()
// Change closure resolver so first the delegate is used.
printInfo.resolveStrategy = Closure.DELEGATE_FIRST
// Set delegate to Post object.
printInfo.delegate = new Post(count: 100

assert "This is Groovy Goodness post #101!" == printInfo()

在这段代码里,我们先创建了一个非常简单的类 Post,它有 count 属性和 info 方法。然后我们又在脚本中定义了 count 和 info 方法。当第一次调用 printInfo 时,它会直接在这个 Script 中找到 count 的值和 info 方法,count 为 0。而为 printInfo 设置了 Delegate 并且设置了 DELEGATE_FIRST 之后,调用 printInfo 时 count 属性和 info 方法会先在 Post 里面找,而在 Post 中 count 等于 100。
因此在执行 Closure 时,它会依赖于它所在的上下文,默认是定义 Closure 的上下文,对 printInfo 来说则是整个 Script,此时调用就会使用 Script 中的 count 和 info。而之后我们将 printInfo 的上下文设成 Post,由于 ResolveStrategy 为 DELEGATE_FIRST,它就会优先使用 Post 里面的属性和方法。
若还有不明白的地方,建议大家读下 Gradle Tips#2-语法Gradle学习系列之三——读懂Gradle语法,这两篇文章都讲得很清楚。

Task & Plugin

在 Java 中,我们习惯于将常用的功能抽象成方法或类,然后在需要的时候 import 使用。
但在 Gradle 中,更标准的做法是将功能抽象成 Task 和 Plugin。以 Android 举例,在 build.gradle 通过 apply plugin: 'com.android.application' 来引入 Plugin,这样就为 Project 引入了相关 Android 的 Task。如何自定义 Plugin 请参考 Writing Custom PluginsGradle学习系列之十——自定义Plugin
不知大家有没有疑惑,我当时就很纳闷 build.gradle 中那些 Script Block 都哪里来的。 Script Block 指的是以 Closure 为参数的方法,如上文例子的 buildscrpt。其实,我们在创建 Task 或者 Extension (给 Project 添加额外的属性,在自定义 Plugin 时经常使用)时,会在 Project 中额外添加以 Task 或 Extension 名字命名的 Script Block。
比如通过 task sayHello 创建了 sayHello 的 task,同时也在 Project 中增加了 sayHello(Closure closure) 的方法。

Debug

开玩笑,Gradle 还能 Debug?为什么不能。
首先在 Terminal 里输入

1
export GRADLE_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005"

此时执行 ./gradlew 会显示 Listening for transport dt_socket at address: 5005
打开 Android Studio,按下图设置。



最后,点击 Debug 按钮就可以开始了。

参考