Android Gradle 构建 - 理解 DSL 语言以及运行机制

2,737 阅读8分钟

前言

这篇文章可能跟Anroid的关系不是很深,主要介绍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里有配置了repositoriesdependencies,而repositoriesdependencies里面又可以配置各自的一些属性。可以看出通过这种形式的配置,我们可以层次分明的看出整个项目构建的一些定制,又由于Android也遵循约定大于配置的设计思想,因此我们仅仅只需修改需要自定义的部分即可轻松个性化构建流程。

Gradle下的Groovy脚本-build.gradle

在Groovy下,我们可以像Python这类脚本语言一样写个脚本文件直接执行而无需像Java那样既要写好Class又要定义main()函数,因为Groovy本身就是一门脚本语言,而Gradle是基于Groovy语言的构建工具,自然也可以轻松通过脚本来执行构建整个项目。作为一个基于Gradle的项目工程,项目结构中的settings.gradlebuild.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方法。其中当做形参的bd可以作为一个闭包(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)来手动解析一下这段脚本含义,下面拆分下这些步骤吧:

  1. 首先email {...}这段被执行,其执行方式等效于email({...}), Groovy调用email方法,然后将{...}这个闭包(Closure)作为参数传递进去;
  2. from 'dsl-guru@mycompany.com'等效于from('dsl-guru@mycompany.com')解析执行;
  3. subject 'The pope has resigned!'等效于subject('The pope has resigned!')解析执行;
  4. body {...}同步骤1一样,{...}这个闭包作为body方法的参数,等效于body({...})解释执行;
  5. 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闭包里所有方法比如fromto等等的一个类(Class),通过rehydrate方法将cl拷贝成一份新的实例(Instance)并赋值给codecode实例(Instance)通过rehydrate方法中设置delegateownerthisObject的三个属性将clemail两者关联起来被赋予了一种委托关系,这种委托关系可以这样理解:cl闭包中的fromto等方法会调用到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点击某个方法我们可以发现buildscriptallprojectstask都指向了Project类,由此可以看出Project类是整个build.gradle脚本文件的委托类,其中必然有一个Project的实例(Instance)在管理这些类,当我们执行诸如biuldscriptallprojectstask这些方法的时候,就能够对这个Project实例进行配置。由此最后Gradle基于Project类的实例(Instance)进行整个项目的构建流程。
接下来描述下这份grade脚本文件的执行步骤,为了描述方便,我将buildscript方法中的闭包(Closure)称为C1,然后其他闭包(Closure)对应关系依次为repositories->C2dependencies->C3all projects->C4repositories->C5,最后一个task...这一部分闭包(Closure)就不定义了,至于原因,你可以猜下~接下来按照步骤来说吧:

  1. 执行buildscript方法,并把C1作为形参传递进去,进行构建脚本的一些配置,此时C1的委托者(The delegate of closure)是Project类中的ScriptHandler的实例(Instance);
  2. 执行C1中的方法,此时执行repositories方法并以C2作为形参,配置仓库地址,C2的委托者(The delegate of closure)是RepositoryHandler类的实例(Instance),负责相关仓库的配置;
  3. 执行C2中的方法,由于C2的委托者(The delegate of closure)是RepositoryHandler的实例(Instance),因此执行了RepositoryHandlerjcenter方法,将它配置成我们项目的远程仓库;
  4. 执行dependencies方法并将C3作为形参,配置一些相关的构建依赖,C3的委托者(The delegate of closure)是DependencyHandler类的实例(Instance);
  5. 执行C3中的方法,同步骤3一样,调用委托者(The delegate of closure)DependencyHandler的方法classpath并把相关依赖作为形参传递过去,不过这里你会发现用IDE进去却是对应add(String configurationName, Object dependencyNotation)这个方法,这里一定有玄机,感兴趣的朋友可以自个探索下;
  6. 同上面原理一样,执行all projectsC4repositoriesC5等这类方法,配置了所有项目工程的仓库为jcenter,这里不再赘述;
  7. 接下来是task clean ...这部分DSL了,根据Groovy的链式命令(Command chains),此处执行的顺序应该是clean([type: Delete], {delete rootProject.buildDir}) -> task(...),由此完成一个Task的创建,由于指定了typeDelete,所以{delete rootProject.buildDir}这个闭包(Closure)的委托者(The delegate of closure)就是Delete类的实例(Instance),具体实现方式可以参考Gradle的源码。

    结语

    至此,你应该对于Android DSL有了一个大概的了解吧。由于本人水平有限,如果其中有错误之处还望指出,233333~