Gradle 学习笔记
本文记录学习 gradle 过程中值得记录的内容。
Groovy 语言
语言简介
- Groovy 语言运行在 JVM 上
- Groovy 代码兼容 Java 代码,也就是可以在 Groovy 中直接写 Java 代码
- Groovy 适合作为 DSL(domain-specific language) 来使用,Android 中使用的就是 Android plugin DSL。
与 Java 之间的差异性
既然是做 Android 开发,肯定是熟悉 Java 语言,而且 Groovy 又是运行在 JVM 上的,可以说内在的运行机制与 Java 是相同的,不同的只有语法,这里说说语法的差异性。
1、写在 build.gradle 中的代码为什么很多花括号?
例如
android {
buildTypes {
debug {
debuggable true
minifyEnabled false
}
}
}
看起来好像一堆罗列的数据配置,实际上是一些可运行的 Groovy 语句。涉及到两个语法:
- 函数调用可以省略括号。
android
其实是一个方法,不省略的时候应该这么写:android({})
,省略后只剩花括号了。 - 闭包。Groovy 中的闭包是一个开放的,匿名的代码块,可以接受参数,可以返回值,也可以赋值给变量——来自官方文档的解释。把闭包当成匿名内部类的一个方法去理解可能更好理解一些,如果了解 javascript 等其他函数语言,与它们的闭包概念是一样的。省略了参数的闭包看起来就是一对花括号。注意闭包是可以赋值给变量的,也就是说可以作为参数传递给一个方法。
因此 android { ... }
实际上是 android
这个方法的调用,这个方法只有一个参数,参数的类型是一个无参数的闭包。
2、build.gradle 第一行代码是怎么回事?
apply plugin: 'com.android.library'
它省略了一些东西,补完再看就明白了:
apply([plugin: 'com.android.library'])
再看一下 apply
方法的原型,它属于 PluginAware
接口:
void apply(Map<String,?> options)
在 Groovy 中 Map 可以定义字面量,比如:[plugin: 'com.android.library'],如果 Map 是方法的第一个参数,那么可以将 map 拆开来写,也就是可以省略 []
。
3、属性和伪装成属性的方法
Groovy 中的属性与 Java 使用不同的约定风格。Java 中遵循 JavaBean 的风格,属性的 getter 和 setter 方法有 get
和 set
的前缀。而在 Groovy 中属性也有 getter 和 setter,但使用的时候直接使用赋值表达式即可。看下面的例子:
android {
defaultConfig {
....
targetSdkVersion 29 // 1
targetSdkVersion = 19 // 2
setTargetSdkVersion(29) // 3
}
}
以上 3 种写法都是正确的,2 和 3 是等价的,但 1 的写法却是另外一个与属性同名的方法,也就是说同时存在 targetSdkVersion(int)
与 setTargetSdkVersion(int)
这两个方法。在 Android Studio 中,如果用 2 的写法会提示你错误,说这样写 Android Studio 可能比较懵逼无法提供更好的支持……默认使用 1 的写法,这种伪装成属性的方法调用也是 Android Gradle DSL 的目的,使这些代码看起来更像配置数据。
4、定义字符串的 N 种方法
有三种方式(与其他比较新的语言很类似)
- 双引号:
"包含在前后各一个引号内"
,可以使用内嵌表达式的语法"log: value={$value}"
- 单引号:
'包含在前后各一个单引号内'
,相当于 Java 中的写法 - 三引号:
"""包含在前后各三个引号内"""
,该方式定义的字符串能包含回车不用写\n
,也可以内嵌表达式
文档在哪里
这里要先说一下 gradle 和 android 的关系。gradle 是一个构建系统,也就是 maven、make 之类的工具,本来与 android 并没有关系,直到 Google 厌倦了 Eclipse,开发了 Android Studio,Android Studio 中直接使用 gradle 作为 android 的构建系统。gradle 是一个强大的可定制的构建系统,而且 gradle 本身也在不断地发展中,能搞定 Android 复杂的构建过程,并满足 Android 不断发展的需求。
Android 使用 Gradle 的具体方案是实现了几个 gradle 插件,有 app plugin 和 lib plugin,它们负责整个 Android 构建过程,我们开发者只需要按照插件的使用规范来使用就可以了。
- 这些插件的使用规范,就是前面提到的 DSL,在这里:Android Plugin DSL Reference。与 Android 相关的东西可以查这里。
- 而 gradle 本身的类文档可以到 gradle 的官网上查看:Gradle User Manuall。与 Android 无关的可以查这里。
- 另外还有 Groovy 语言文档:The Apache Groovy programming languagel。Groovy 语法可以查这里。
如何打印 log
打印 log 是学习的第一步,学习中遇到的问题,都可以打个 log 验证。
println "source set: ${it} : ${it.class}" + ', name=' + it.name
可以看到有一些特殊的 ${}
符号,这个是字符串内嵌表达式的语法,在双引号内才能使用,方便一些复杂的字符串的拼接。当然直接使用 +
来拼接也可以。
关于 SourceSet 的那些事
什么是 SourceSet?
SourceSet 就是源码的集合。用来告诉 gradle 我的 java 代码在哪里,我的 resource 目录在哪里等等。默认情况下,只有一个 SourceSet : main,也不用做任何设置就能正常编译项目,实际上是因为 SourceSet 规定了一些默认值,如果按照默认值配置项目(Android Studio 新建项目就按照默认值创建)就不用额外去写配置代码了。
SourceSet 规定的那些默认值是什么?
这个可以通过运行一个 task 来查看,这个 task 就叫 sourceSets
,可以在 Android Studio 的 gradle 面板上双击执行,也可以用命令行执行。见下图:

执行结果片段:
main
----
Compile configuration: compile
build.gradle name: android.sourceSets.main
Java sources: [app\src\main\java]
Manifest file: app\src\main\AndroidManifest.xml
Android resources: [app\src\main\res]
Assets: [app\src\main\assets]
AIDL sources: [app\src\main\aidl]
RenderScript sources: [app\src\main\rs]
JNI sources: [app\src\main\jni]
JNI libraries: [app\src\main\jniLibs]
Java-style resources: [app\src\main\resources]
上面列出的是 main 的默认值,默认情况下还应该打印出这几个的默认值: debug、release、test、testDebug、testRelease、androidTest、androidTestDebug。带 test 字符串的是与测试相关的。debug 和 release 也是默认就创建好的两个 SourceSet。
多个 SourceSet 之间的关系是什么?
先说 main 这个 SourceSet,它是一个共用的 SourceSet,可以认为是所有 SourceSet 的“基类”,任何一个其他的 SourceSet 编译时都会将 main 中的代码包括进去,也就是说 main 中的代码肯定是会参与编译的。
再说 debug 和 release 这两个 SourceSet,他俩是默认的两个 buildType 生成的 SourceSet。buildType 会影响 SourceSet,而且 buildType 与 SourceSet 是一一对应的,只要有一个 buildType 就会有一个 SourceSet 默默地创建出来。可以将这种 SourceSet 记为 buildType 维度的 SourceSet。
如果定义了 productFlavor,如下面代码所示:
android {
productFlavors {
huoguo {
}
malatang {
}
}
}
就会生成两个名为 huoguo 和 malatang 的 SourceSet。productFlavor 也是与 SourceSet 一一对应的。可以将这种 SourceSet 记为 productFlavor 维度的 SourceSet。
那么最终编译的时候到底应该使用哪个 SourceSet?这就要提到 Build Variant,在 Android Studio 中可以在 Build Variants 界面选择,如下图所示:

可以看到两个不同维度的 SourceSet 自由组合了起来,生成了 2 x 2 = 4 个 Build Variant。每次编译只能选择其中一种,也就是说,最终编译使用哪些 SourceSet 是由 buildType 和 productFlavor 共同决定的,而 SourceSet 就起到了区分这些变量的作用,通过 SourceSet 的配置,使得不同的 buildType 和不同的 productFlavor 有机会使用不同的代码来编译。
回到 SourceSet 之间的关系的问题,同一维度下的几个 SourceSet 之间可见是互斥的关系,而不同维度之间的 SourceSet 就是(可能产生的)组合的关系。因此同一维度下的不同 SourceSet 可以使用路径完全相同的类文件,而不用担心类重复的冲突。而不同维度下的 SourceSet 必须考虑到冲突的问题,不能使用路径相同的类文件。
SourceSet 自定义路径
上文提到的 sourceSets
task 会打印出所有的目录默认配置,列出的就是所有 SourceSet 可配置的目录,对应的代码如下摘自 DSL 文档的图:

下面举几个常用的例子:
例1:添加一个 java 源码目录的设置
android {
sourceSets {
main { // main source set
java {
def anotherDir = "......"
srcDirs anotherDir // 1 添加
srcDir anotherDir // 2 添加
setSrcDirs([anotherDir]) // 3 重置
srcDirs = [anotherDir] // 4 重置
srcDirs += [anotherDir] // 5 添加
}
}
}
}
注释 1 与 2 位置的语句是调用了 srcDirs(dir)
和 srcDir(dir)
这两个方法,虽然方法名有单数和复数的区别,实际上这两个方法效果是完全一样的,都是添加一个源码目录。而注释 3 和 4 的语句是等价的,是设置属性 srcDirs,会覆盖之前的值,也就是说设置完成后,只剩下新添加的 anotherDir 了。注释 5 的语句相当于调用 setSrcDirs(getSrcDirs() + [anotherDir]),使用了运算符重载(operator overload),重载了 +
运算,与 1 和 2 是效果相同的。
例2:将 SourceSet 的路径修改为另一套
android {
sourceSets {
main {
def dependSrc = 'juanbing'
java.srcDirs = ["${dependSrc}/java"]
assets.srcDirs = ["${dependSrc}/assets"]
res.srcDirs = ["${dependSrc}/res"]
manifest.srcFile "${dependSrc}/AndroidManifest.xml"
}
}
}
网友评论