前言
这篇文章可能跟Android的关系不是很深,主要介绍Groovy是如何一步步解析Android的DSL语言,这样你在配置一些Gradle文件的时候可以更加得心应手。阅读本文之前你需要具有一点Android基础,并且需要了解一些Groovy语言的基本特性,例如Closure
、[]
, def
等含义。Groovy是一种运行在JVM虚拟机上的脚本语言,能够与Java语言无缝结合,如果想了解Groovy可以查看IBM-DeveloperWorks-精通Groovy。
DSL的好处
我们打开Android的build.gradle
文件,会看到类似下面的一些语法:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.5.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
这是一个简单的build.gradle
配置文件,我们可以看到buildscript
里有配置了repositories
和dependencies
,而repositories
和dependencies
里面又可以配置各自的一些属性。可以看出通过这种形式的配置,我们可以层次分明的看出整个项目构建的一些定制,又由于Android也遵循约定大于配置的设计思想,因此我们仅仅只需修改需要自定义的部分即可轻松个性化构建流程。
Gradle下的Groovy脚本-build.gradle
在Groovy下,我们可以像Python这类脚本语言一样写个脚本文件直接执行而无需像Java那样既要写好Class又要定义main()
函数,因为Groovy本身就是一门脚本语言,而Gradle是基于Groovy语言的构建工具,自然也可以轻松通过脚本来执行构建整个项目。作为一个基于Gradle的项目工程,项目结构中的settings.gradle
和build.gradle
这类xxx.gradle
可以理解成是Gradle构建该工程的执行脚本,当我们在键盘上敲出gradle clean aDebug
这类命令的时候,Gradle就会去寻找这类文件并按照规则先后读取这些gradle文件并使用Groovy去解析执行。
让DSL"活起来"-Groovy的魔法
要理解build.gradle
文件中的这些DSL是如何被解析执行的,需要介绍Groovy的一些语法特点以及一些高级特性,官方有一篇关于DSL特性的描述,如果你追求原味直接看这个即可。 Domain-Specific-Languages
下面将介绍一下比较重要的几个特点。
Command chains - 链式命令
Groovy的脚本具有链式命令(Command chains)的特性,根据这个特性,当你在Groovy脚本中写出a b c d
的时候,Groovy会翻译成a(b).c(d)
执行,也就是将b
作为a
函数的形参调用,然后将d
作为形参再次调用返回的实例(Instance)中的c
方法。其中当做形参的b
和d
可以作为一个闭包(Closure)传递过去。
下面是一些简单的实例:
// equivalent to: turn(left).then(right)
turn left then right
// equivalent to: take(2.pills).of(chloroquinine).after(6.hours)
take 2.pills of chloroquinine after 6.hours
// equivalent to: paint(wall).with(red, green).and(yellow)
paint wall with red, green and yellow
// with named parameters too
// equivalent to: check(that: margarita).tastes(good)
check that: margarita tastes good
// with closures as parameters
// equivalent to: given({}).when({}).then({})
given { } when { } then { }
Groovy也支持某个方法传入空参数,但需要为该空参数的方法加上圆括号。
// equivalent to: select(all).unique().from(names)
select all unique() from names
如果链式命令(Command chains)的参数是奇数,则最后一个参数会被当成属性值(Property)访问。
// equivalent to: take(3).cookies
// and also this: take(3).getCookies()
take 3 cookies
Operator overloading - 操作符重载
有了Groovy的操作符重载(Operator overloading),==
会被Groovy转换成equals
方法,这样你就可以放心大胆地使用==
来比较两个字符串是否相等了,在我们编写gradle脚本的时候也可以尽情使用。关于Groovy的所有操作符重载(Operator overloading)可以查阅Operator overloading。
DelegatesTo - 委托
委托(DelegatesTo)可以说是Gradle选择Groovy作为DSL执行平台的一个重要因素了。通过委托(DelegatesTo)可以很简单的定制一个控制结构体(Custom control structures),比如你可以写如下这段代码:
email {
from 'dsl-guru@mycompany.com'
to 'john.doe@waitaminute.com'
subject 'The pope has resigned!'
body {
p 'Really, the pope has resigned!'
}
}
上面这段代码便是我们自己定义的DSL语言了,当然这也是一段脚本,我们可以结合上文讲到的Groovy的链式命令(Command chains)来手动解析一下这段脚本含义,下面拆分下这些步骤吧:
- 首先
email {...}
这段被执行,其执行方式等效于email({...})
, Groovy调用email
方法,然后将{...}
这个闭包(Closure)作为参数传递进去; -
from 'dsl-guru@mycompany.com'
等效于from('dsl-guru@mycompany.com')
解析执行; -
subject 'The pope has resigned!'
等效于subject('The pope has resigned!')
解析执行; -
body {...}
同步骤1一样,{...}
这个闭包作为body
方法的参数,等效于body({...})
解释执行; -
p 'Really, the pope has resigned!'
等效于p('Really, the pope has resigned!')
解释执行。
当然,有个问题我们需要清楚,当我们调用email {...}
这种方法的时候,{...}
闭包中的方法比如from 'dsl-guru@mycompany.com'
等不是Groovy Shell自动去调用执行的,而是通过Groovy的委托(DelegatesTo)方式来完成,这块后文会讲到。
接下来我们可以看下解析上述DSL语言的代码:
def email(Closure cl) {
def email = new EmailSpec()
def code = cl.rehydrate(email, this, this)
code.resolveStrategy = Closure.DELEGATE_ONLY
code()
}
我们先定义了一个email(Closure)
的方法,当执行上述步骤1的时候就会进入该方法内执行,EmailSpec
是一个继承了参数中cl
闭包里所有方法比如from
、to
等等的一个类(Class),通过rehydrate
方法将cl
拷贝成一份新的实例(Instance)并赋值给code
,code
实例(Instance)通过rehydrate
方法中设置delegate
、owner
和thisObject
的三个属性将cl
和email
两者关联起来被赋予了一种委托关系,这种委托关系可以这样理解:cl
闭包中的from
、to
等方法会调用到email
委托类实例(Instance)中的方法,并可以访问到email
中的实例变量(Field)。DELEGATE_ONLY
表示闭包(Closure)方法调用只会委托给它的委托者(The delegate of closure),最后使用code()
开始执行闭包中的方法。
当然,Groovy提供了很多灵活的委托(DelegatesTo)方式,这块可以通过阅读官方文档了解。
Android DSL解读
下面我们直接开始解读上文提供的build.gradle
这个文件,让我们来看看Groovy是如何让这些DSL发挥了作用。
build.gradle:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.5.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
可以看到这份build.gradle
依次执行了buildscript({...})
、all projects{...}
、all projects{...}
和task...
方法。通过Android Studio
点击某个方法我们可以发现buildscript
、allprojects
和task
都指向了Project
类,由此可以看出Project
类是整个build.gradle
脚本文件的委托类,其中必然有一个Project
的实例(Instance)在管理这些类,当我们执行诸如biuldscript
、allprojects
和task
这些方法的时候,就能够对这个Project
实例进行配置。由此最后Gradle基于Project
类的实例(Instance)进行整个项目的构建流程。
接下来描述下这份grade脚本文件的执行步骤,为了描述方便,我将buildscript
方法中的闭包(Closure)称为C1
,然后其他闭包(Closure)对应关系依次为repositories
->C2
、dependencies
->C3
、all projects
->C4
,repositories
->C5
,最后一个task...
这一部分闭包(Closure)就不定义了,至于原因,你可以猜下~接下来按照步骤来说吧:
- 执行
buildscript
方法,并把C1
作为形参传递进去,进行构建脚本的一些配置,此时C1
的委托者(The delegate of closure)是Project
类中的ScriptHandler
的实例(Instance); - 执行
C1
中的方法,此时执行repositories
方法并以C2
作为形参,配置仓库地址,C2
的委托者(The delegate of closure)是RepositoryHandler
类的实例(Instance),负责相关仓库的配置; - 执行
C2
中的方法,由于C2
的委托者(The delegate of closure)是RepositoryHandler
的实例(Instance),因此执行了RepositoryHandler
的jcenter
方法,将它配置成我们项目的远程仓库; - 执行
dependencies
方法并将C3
作为形参,配置一些相关的构建依赖,C3
的委托者(The delegate of closure)是DependencyHandler
类的实例(Instance); - 执行
C3
中的方法,同步骤3一样,调用委托者(The delegate of closure)DependencyHandler
的方法classpath
并把相关依赖作为形参传递过去,不过这里你会发现用IDE进去却是对应add(String configurationName, Object dependencyNotation)
这个方法,这里一定有玄机,感兴趣的朋友可以自个探索下; - 同上面原理一样,执行
all projects
、C4
、repositories
和C5
等这类方法,配置了所有项目工程的仓库为jcenter
,这里不再赘述; - 接下来是
task clean ...
这部分DSL了,这块的逻辑存在一个比较奇怪的问题,根据Groovy的链式命令(Command chains),此处执行的顺序应该是clean([type: Delete], {delete rootProject.buildDir})
->task(...)
,然而实际上并非如此,其实际执行应该是task([type: Delete], 'clean', {delte rootProject.buildDir})
(此处仅个人理解,感谢@花京院典明 指正,之后有时间把这块 DSL 解析过程完善下),由此完成一个Task的创建,由于指定了type
为Delete
,所以{delete rootProject.buildDir}
这个闭包(Closure)的委托者(The delegate of closure)就是Delete
类的实例(Instance),具体实现方式可以参考Gradle的源码。
结语
至此,你应该对于Android DSL有了一个大概的了解吧。由于本人水平有限,如果其中有错误之处还望指出,233333~