在Android开发中,很多时候我们不需要修改 *.gradle 文件太多,我们添加依赖、修改target compile、最低支持API level,或者修改签名配置和build类型。其它更复杂一些逻辑,我们最后可能就是从Stack Overflow中copy了一些自己也不太懂的代码。本文中我们将一步一步介绍Android工程中用到的gradle文件及其背后的原理。
1. Groovy
1.1 语法
Gradle文件其实是用Groovy脚本写的,我们都会写java,所以入门Groovy非常简单。首先我们需要了解一下几点:
1. 调用至少包含一个参数的方法时不需要使用括号:
def printAge(String name, int age) { print("$name is $age years old") }def printEmptyLine() {
println()
}def callClosure(Closure closure) {
closure()
}printAge "John", 24 // Will print "John is 24 years old"
printEmptyLine() // Will, well, print empty line
callClosure { println("From closure") } // Will print "From closure"
2. 如果方法的最后一个参数是闭包(或者说是 lambda 表达式),可以写在括号外(注:这个特性很重要,gradle 文件中的很多配置其实都是参数为闭包的方法):
def callWithParam(String param, Closure<String> closure) {
closure(param)
}callWithParam("param", { println it }) // Will print "param"
callWithParam("param") { println it } // Will print "param"
callWithParam "param", { println it } // Will print "param"
3. 对于 Groovy 方法中命名过的参数,会被转移到一个 map 中做为方法的第一个参数,那些没有命名的参数则加在参数列表之后:
def printPersonInfo(Map<String, Object> person) {
println("{person.name} is {person.age} years old")
}def printJobInfo(Map<String, Object> job, String employeeName) {
println("employeeName works as {job.title} at ${job.company}")
}printPersonInfo name: "John", age: 24
printJobInfo "John", title: "Android developer", company: "Tooploox"
这段程序会打印“John is 24 years old”和“John works as Android developer at Tooploox”,方法调用的参数可以是乱序的,map 会被作为第一个参数传入!这里的方法调用也省略了括号。
1.2 闭包
闭包是一个非常重要的特性,需要解释一下。闭包可以理解为 lambada。他们是一段可以被执行的代码,可以有参数列表和返回值。我们可以改变一个闭包的委托:
class WriterOne {
def printText(str) {
println "Printed in One: $str"
}
}class WriterTwo {
def printText(str) {
println "Printed in Two: $str"
}
}def printClosure = {
printText "I come from a closure"}printClosure.delegate = new WriterOne()
printClosure() // will print "Printed in One: I come from a closure
printClosure.delegate = new WriterTwo()
printClosure() // will print "Printed in Two: I come from a closure
我们可以看到printClosure
调用了不同委托的printText
方法,之后会解析这个特性在 gradle 中的重要性。
2. Gradle
2.1 脚本文件
有三个主要的 gradle 脚本,每个都是一个代码块。
2.2 Projects
gradle 构建一般包含多个 Project(在 Android 中每个 module 对应这里的 project),project 中包含 tasks。一般至少有一个 root project,包含很多 subprojects,subproject 也可以嵌套 project(注:Android 中对应每个 library module 还可以依赖其它 library module)。
3. 构建基于 Gradle 的 Android 工程
Android 工程中我们一般有如下的结构:
1 是 root project 的 setting 文件,被Settings
执行
2 是 root project 的 build 配置
3 是 App project 的属性文件,会被注入到 App 的Settings
中
4 是 App project 的 build 配置
3.1 创建 gradle 工程
我们新建一个文件夹,命名为example
,cd
进入后执行gradle projects
命令,之后就已经拥有一个 gradle project 了:
$ gradle projects
:projectsRoot project
Root project 'example'No sub-projects
To see a list of the tasks of a project, run gradle <project-path>:tasks
For example, try running gradle :tasks
BUILD SUCCESSFUL
Total time: 0.741 secs
3.2 配置 projects 层级
如果我们要建立一个默认的 Android project(空的 root project 和一个包含 Application 的 app project),我们就需要配置settings.gradle
, the documentation 中介绍settings.gradle
:
声明需要实例化的配置和 build 的 project 的层级体系配置
我们通过 void include(String[] projectPaths)方法来添加 projects:
这里的冒号:
用于分隔子 project,可以参考这里 here。因此我们在这里写:app
, 而不是直接写app
。
在settings.gradle
中写rootProject.name = <<name>>
也是一个比较好的实践。如果没有写,那么 root project 的默认名字就是 project 所在文件夹的名字。
3.3 配置 Android 子 project
我们已经配置了 root project 的build.gradle
,现在来看看如何配置 Android project。
从user guide可以知道我们首先要为 app project 配置com.android.application
插件,我们来看看apply
方法:
void apply(Closure closure)
void apply(Map<String, ?> options)
void apply(Action<? super ObjectConfigurationAction> action)
尽管第三个方法很重要,我们通常使用是第二个方法,它用到我们之前提到的特性,通过 map 来传递参数。通过文档我们可以查看可以使用哪些参数:
void apply(Map(<String, ?> options)
以下是可用的参数:
from: 可以引入一个脚本 apply(...),如apply from: "bintray.gradle"
从而导入一个可用脚本。
plugin: apply 的 plugin 的 id 或者实现类
to: 委托目标
我们知道需要传递一个 id 值作为plugin
的参数,可以写作:apply(plugin:'com.android.application')
,这里的括号也可以省略,我们在 app 的build.gradle
中配置:
命令行中运行:
报错了,找不到com.android.application
的定义,这不奇怪,我们并没有配置,但是 gradle 是如何查找 Android 的 plugin jar 包呢?在user guide可以找到答案,我们需要配置 plugin 的路径。
现在我们可以在 root project 或者 app 的build.gradle
中配置路径,但是因为buildscript
闭包是ScriptHandler
执行的,其它子 project 也需要使用,因此最好配置在 root project 的build.gradle
中:
buildscript {
repositories {
jcenter()
}dependencies {
classpath 'com.android.tools.build:gradle:2.3.0-beta2'
}
}
如果我们在上边的代码中添加括号,那么就会发现其实都是带有闭包参数的方法调用。如果我们研究下 文档,我们就可以知道是有哪些对象执行这些闭包的,总结如下:
buildscript(Closure)
是Project
实例中调用的,传递的闭包的由ScriptHandler
执行.repositories(Closure)
是在ScriptHandler
实例中调用,传递的闭包由RepositoryHandler
执行.dependencies(Closure)
是在ScriptHandler
实例中调用,传递的闭包由DependencyHandler
执行。
也就是说 jcenter()
是由 RepositoryHandler
调用 classpath(String)
是由 DependencyHandler(*)
调用
译者注:如果这里看不懂的同学,可以再回头看看 groovy 的语法部分,其实这里上边的代码都是方法,如 buildscript 是 Project 的方法,我们知道 groovy 语法中如果最后一个参数是闭包的话,可以不写括号。
如果查看DependencyHandler
的代码,我们会发现其实没有classpath
这个方法,这是一种特殊的调用,我们在稍后讨论。
3.4 配置 Android subproject
如果我们现在执行 Gradle task,依然有错误:
显然,我们还没有设置 Android 相关的配置,但是我们的 Android plugin 已经可以被正确 apply 了,我们增加一些配置:
android {
buildToolsVersion "25.0.1"
compileSdkVersion 25
}
到这里我们知道,android 方法被加入到了Project
实例中,闭包传递给了 delegate(这里是 AppExtension),定义了buildToolsVersion
和 compileSdkVersion
方法,Android plugin 使用这种方式接收所有的配置,包括 default configuration,flavors 等等。
想要执行 gradle task,还需要两个文件:AndroidManifest.xml
和 local.properties
,local.properties
中配置sdk.dir
,(或者在系统环境中配置ANDROID_HOME
),指向 Android SDK 的位置。
3.5 扩展
android
方法是如何出现在Project
实例中的呢,还有我们的 build.gradle 是怎样被执行的?简单的说,Android plugin 用 android 这个名字注册AppExtension
类为extension
。这个超出了本文的范围,但是我们要知道 Gradle 可以为每一个注册过的 plugin 增加闭包配置。
3.6 依赖
还有一个重要的部分,dependencies 还没有讨论:
dependencies {
compile 'io.reactivex.rxjava2:rxjava:2.0.4'
testCompile 'junit:junit:4.12'
annotationProcessor 'org.parceler:parceler:1.1.6'
}
为什么这里特殊呢,因为如果查看DependencyHandler,也就是执行这个闭包的委托,它是没有compile
,testCompile
等方法的。这个问题是有意义的,如果我们随意增加一个freeCompile 'somelib'
,可以吗?DependencyHandler
不会定义所有的方法,其实这里涉及到 Groory 语音的另一个特性:methodMissing,这允许在运行时 catch 对于未定义方法的调用。
实际上 Gradle 使用了MethodMixIn中声明的methodMissing
,类似的机制在为定义的属性中也是一样的。
相关的 dependency 操作可以在 这里找到,它的行为如下:
如果未定义方法的调用方有至少一个参数,如果存在configuration()
与被调用方法有相同的名字,那么就根据参数的类型和数量,调用具有相关参数的doAdd
方法。
每个 plugin 都可以增进 configuration 到 dependencies handler 中,如 Android 插件增加了compile, compileClasspath, testCompile
和一些其它配置here,Android 插件还增加了annotationProcessor
配置,根据不同 build 类型和产品形式还有<variant>Compile, <variant>TestCompile
等等。
由于doAdd
是私有方法,一次这里调用的是公有的add
方法,我们可以重写上边的代码,但最后不要这样做:
dependencies {
add('compile', 'io.reactivex.rxjava2:rxjava:2.0.4')
add('testCompile', 'junit:junit:4.12')
add('annotationProcessor', 'org.parceler:parceler:1.1.6')
}
3.7 Flavors, build types, signing configs
我们看以下代码:
productFlavors {
prod {}
dev {
minSdkVersion 21
multiDexEnabled true
}
}
如果我们查看源码,可以发现 productFlavors 是这样声明的:
void productFlavors(Action<? super NamedDomainObjectContainer<ProductFlavorDsl>> action) {
action.execute(productFlavors)
}
Action<T>
是 Gradle 中定义的由T
执行的闭包
所有这里我们有了NamedDomainObjectContainer
,NamedDomainObjectContainer
可以创建和配置多个ProductFlavorDsl
类型的对象,并根据ProductFlavorDsl
的名字保存ProductFlavorDsl
。
这个容器可以使用动态方法创建指定类型的对象(这里的 ProductFlavorDsl),并和名字一起存放在容器中,所以当我们使用{}
参数调用prod
方法时,他被productFlavors
实例执行,执行说明如下:
NamedDomainObjectContainer
获取到被调用方法的名字,生成ProductFlavorDsl
对象,配置给定的闭包,保存方法名字到新的配置ProductFlavorDsl
的映射。
Android plugin 可以从productFlavors
中获取ProductFlavorDsl
,我们可以把它作为属性进行访问:productFlavors.dev
,这样我们就可以拿到名字为dev
的ProductFlavorDsl
,这也是我们可以写signingConfig
signingConfigs.debug
的原因。
4. 总结
对于 Android 开发者来说,Gradle 文件是非常常用的,并不是什么黑魔法。但是 Gradle 有很多约定,而且使用 Groovy 语言也增加了一些复杂性,知道这两点,Gradle 并不是什么魔法。希望了解通过这篇文章介绍的内容,即使是从 stackoverflow 中粘贴代码,也能知道它背后的意义。
这是一篇译文,原文作者对 Android 的 gradle 进行了比较深入的介绍,希望各位同学可以真正了解我们常用的 gradle 文件背后的原理,而不仅仅是简单地配置 gralde。文中有些不太容易理解的地方,可以根据文中给出的链接了解更多内容。
原文地址https://medium.com/@wasyl/understanding-android-gradle-build-files-e4b45b73cc4c#.svvmjs12o
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于