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

作者: 灵丞 | 来源:发表于2016-09-02 23:15 被阅读2180次

    前言

    这篇文章可能跟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里有配置了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([type: Delete], 'clean', {delte rootProject.buildDir})(此处仅个人理解,感谢@花京院典明 指正,之后有时间把这块 DSL 解析过程完善下),由此完成一个Task的创建,由于指定了typeDelete,所以{delete rootProject.buildDir}这个闭包(Closure)的委托者(The delegate of closure)就是Delete类的实例(Instance),具体实现方式可以参考Gradle的源码。

    结语

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

    相关文章

      网友评论

      • 花京院典明:task clean(type:delete) {
        delete rootProjcet.buildDir
        }
        这里的解释是错误的.这里的语法算是Gradle特殊的DSL.实际上是调用了方法task(Map<String,?> params,String name,Closure config).所以clean(type:delete)这里是task的两个参数,而不是clean方法参数为[type:delete],{}.这里的clean是task名.
        灵丞:@花京院典明 恩,这块逻辑看来只能读 Gradle 源码来查了,有时间我跟踪下 Gradle 源码,再次感谢~:smile:
        花京院典明:@BogerChan 这个我个人理解为clean(type:delete)会被解析成 [type:delete],clean.这行代码以groovy的写法应该是task [type:delete],clean {}
        具体的逻辑网上都查不到解释,只能用Gradle特有的DSL来解释了
        灵丞:感谢指正,这里有个疑问哈,这段特殊的DSL处理逻辑 Gradle是如何处理的?task clean(type: delete) 中 "type: delete" 是一个Map,他们如何与 task(Map<String,?> params,String name,Closure config) 作对应?:smiley:
      • __huazhou:多谢
        灵丞:@州轴走宙 感谢支持~ :blush:
      • 捡淑:马克
      • 伊祁放勋:帮了大忙,,感谢
        灵丞:@伊祁放勋 :smile:

      本文标题:Android Gradle构建-理解DSL语言以及运行机制

      本文链接:https://www.haomeiwen.com/subject/bmdcettx.html