Gradle学习总结——根本上看透Android Studio构建

用过android studio的对gradle应该都不陌生了,gradle文件的基本配置大同小异,略做了解使用应该是没什么问题了。但是深入细致的了解一下对于理解项目还是很有帮助的,尤其是遇到一些配置复杂的github项目,不了解gradle可能会遇到跑不起来又束手无策的情形。下面对gradle相关知识、用法做一下总结。

DSL (domain specific language)

即所谓领域专用语言,其基本思想是“求专不求全”,不像通用目的语言那样目标范围涵盖一切软件问题,而是专门针对某一特定问题的计算机语言。
-DSL之于程序员正如伽南地之于以色列人,是最初也是最终的梦想。几乎自计算机发明伊始,人们就开始谈论DSL使用DSL了。
-前几年迅速走红的Ruby on Rails就被誉为“Web开发领域专用语言”

DSL 约等于 整洁的代码

从概念上说,程序的编写过程就是把业务领域中的问题通过代码或者程序模型表达出来:
计算机的程序模型较为单一(归根结底都是运算和存储)
在面向对象技术成为主流的今天,通常情况下,计算机程序不太可能做到与业务领域中的概念一致,或者具有某些直觉的对应。因此,软件的修改和可维护性并没有想象中的容易。我们必须不断地将业务领域中的概念转换成相应的代码模型,然后再进行修改。这种间接性直接造成了软件的复杂度。
而DSL的主要目的就是要消除这样的复杂度(或者说,以构造DSL的复杂度代替这种复杂度),DSL就要是要以贴近业务领域的方式来构造软件。因此,DSL的简洁性往往是一种思维上的简洁性,使我们不用费太多的气力就能看懂代码所对应的业务含义。

DSL多以文本代码的形式出现

多年来软件工程实践表明文本代码是最有效率的编辑形式。但是一些特殊领域,文本代码并不是最佳的表现形式,为了更好的贴近业务领域中的概念,我们可能会选择使用一些图形化的DSL。如:[DSM(Domain Specific Modeling)工具GEMS(Generic Eclipse Modeling System)中就大量地使用了不同的图形化的DSL来表述系统的各个不同侧面。

Gradle向我们提供了一整套DSL,所以在很多时候我们写的代码似乎已经脱离了groovy,但是在底层依然是执行的groovy
为了从命令行运行gradle测试样例,首先

配置环境变量

  1. 创建变量名:GRADLE_HOME ,变量值:
    C:\Users\jjx.gradle\wrapper\dists\gradle-2.5-all\d3xh0kipe7wr2bvnx5sk0hao8\gradle-2.5
  2. 加入path
    ;%GRADLE_HOME%\bin;
  3. 检查,如下就ok。
C:\Users\jjx>gradle -v
Gradle 2.5
Build time:   2015-07-08 07:38:37 UTC
Build number: none
Revision:     093765bccd3ee722ed5310583e5ed140688a8c2b
Groovy:       2.3.10
Ant:          Apache Ant(TM) version 1.9.3 compiled on December 23 2013
JVM:          1.7.0_75 (Oracle Corporation 24.75-b04)
OS:           Windows 7 6.1 amd64

初尝禁果

在d:盘建个文件夹Test,文件夹下建个文件build.gradle
打开文件,写个简单的代码

task helloWorld << {
    println "Hello World"
}

打开cmd, d: 执行gradle helloWorld

C:\Users\jjx>d:

D:\>cd Test

D:\Test>gradle helloWorld
:helloWorld
Hello World

BUILD SUCCESSFUL

Total time: 3.714 secs
D:\Test>

同时,这时候发现已经自动在Test目录下创建了.gradle文件。
上面helloWorld后的“<<”表示追加的意思,即向helloWorld中加入执行过程
使用doLast可以达到同样效果

task helloWorldTwo {
   doLast {
      println 'helloWorldTwo'}
}

如果需要向Task的最前面加入执行过程,我们可以使用doFirst:

task helloWorldThree {
   doFirst {
      println 'helloWorldThree'}
}

耶,懂了!

关于 task

Gradle将当前目录下的build.gradle文件作为项目的构建文件。在上面的例子中,我们创建了一个名为helloWorld的Task,在执行gradle命令时,我们指定执行这个helloWorld Task。这里的helloWorld是一个DefaultTask类型的对象,这也是定义一个Task时的默认类型,当然我们也可以显式地声明Task的类型,甚至可以自定义一个Task类型
下面再看一个小case:
我们在Test文件夹下建一个src目录,建一个dst目录,src目录下建立一个文件,命名为here.txt
然后在build.gradle中append一个task:

task helloWorld << {
    println "Hello World"
}
task copyFile(type: Copy){
    from "src"
    into "dst"
}

代码中(type:Copy)就是“显式地声明Task的类型”,helloworld没有就是默认得DefaultTask类型咯。

然后cmd中执行命令

D:\Test>gradle copyFile
:copyFile

BUILD SUCCESSFUL

Total time: 2.645 secs
D:\Test>gradle copyFile
:copyFile

BUILD SUCCESSFUL

Total time: 3.361 secs
D:\Test>

好了! here.txt也跑到dst中去啦!简单吧!

加一把火,我们来看一下当前目录下(即Test目录文件,这里也可以将这个目录理解为一个project,不过还没写有生产力的代码,哈哈哈)定义的task

D:\Test>gradle tasks
:tasks

------------------------------------------------------------
All tasks runnable from root project
------------------------------------------------------------

Build Setup tasks
-----------------
init - Initializes a new Gradle build. [incubating]
wrapper - Generates Gradle wrapper files. [incubating]

Help tasks
----------
components - Displays the components produced by root project 'Test'. [incubating]
dependencies - Displays all dependencies declared in root project 'Test'.
dependencyInsight - Displays the insight into a specific dependency in root project 'Test'.
help - Displays a help message.
model - Displays the configuration model of root project 'Test'. [incubating]
projects - Displays the sub-projects of root project 'Test'.
properties - Displays the properties of root project 'Test'.
tasks - Displays the tasks runnable from root project 'Test'.

Other tasks
-----------
copyFile
helloWorld

To see all tasks and more detail, run gradle tasks --all

To see more detail about a task, run gradle help --task <task>

BUILD SUCCESSFUL

Total time: 2.895 secs
D:\Test>

是不是很清晰呢,展示了各种task类型,task的作用,以及另外两个task相关的命令,相信聪明的你也一看就懂。

Gradle本身的领域对象主要有Project和Task。
Project为Task提供了执行上下文,所有的Plugin要么向Project中添加用于配置的Property,要么向Project中添加不同的Task。
一个Task表示一个逻辑上较为独立的执行过程,比如编译Java源代码,拷贝文件,打包Jar文件,甚至可以是执行一个系统命令或者调用Ant。另外,一个Task可以读取和设置Project的Property以完成特定的操作。是不是很屌的样子。
task关键字其实是一个groovy中方法的调用,该方法属于Project,而大括号之间的内容则表示传递给task()方法的一个闭包。

task间的依赖

task之间是可以存在依赖关系,比如TaskA依赖TaskB,那么在执行TaskA时,Gradle会先执行TaskB,再执行TaskA。我们可以在定义一个Task的同时声明它的依赖关系:

task helloWorldFour(dependsOn:helloWorldThree) << {
    println 'hellohelloWorldFour'
}

或者

task helloWorldFour << {
    println 'hellohelloWorldFour'
}
helloWorldFour.dependsOn helloWorldThree
可配置的task

一个Task除了执行操作之外,还可以包含多个Property,其中有Gradle为每个Task默认定义了一些Property,比如description,logger等。
另外,每一个特定的Task类型还可以含有特定的Property,比如Copy的from和to等。
当然,我们还可以动态地向Task中加入额外的Property。在执行一个Task之前,我们通常都需要先设定Property的值。

task helloWorld << {
   description = "this is helloWorld" 
   println description
}

或者通过调用Task的configure()方法完成Property的设置:

task helloWorld << {
   println description
}
helloWorld.configure {
   description = "this is helloWorld" 
}
花式task
task showDescription1 << {
   description = 'this is task showDescription'
   println description
}

task showDescription2 << {
   println description
}
showDescription2.description = 'this is task showDescription'

task showDescription3 << {
   println description
}
showDescription3 {
   description = 'this is task showDescription'
}

对于每一个Task,Gradle都会在Project中创建一个同名的Property,所以我们可以将该Task当作Property来访问,showDescription2便是这种情况。另外,Gradle还会创建一个同名的方法,该方法接受一个闭包,我们可以使用该方法来配置Task,showDescription3便是这种情况。
耶!简单吧

关于 Groovy

Gradle是一种声明式的构建工具。
在执行时,Gradle并不会一开始便顺序执行build.gradle文件中的内容,而是分为两个阶段,第一个阶段是配置阶段,然后才是实际的执行阶段。
配置阶段,Gradle将读取所有build.gradle文件的所有内容来配置Project和Task等,比如设置Project和Task的Property,处理Task之间的依赖关系等。
Gradle的DSL只是Groovy语言的内部DSL,也必须遵循Groovy的语法规则。
Groovy语言中的两个概念,一个是Groovy中的Bean概念,一个是Groovy闭包的delegate机制。

bean

Groovy中的Bean和Java中的Bean有一个很大的不同,即Groovy动态的为每一个字段都会自动生成getter和setter,并且我们可以通过像访问字段本身一样调用getter和setter

class GroovyBeanExample {
   private String name
}

def bean = new GroovyBeanExample()
bean.name = 'this is name' //实际调用的是"bean.setName('this is name')"
println bean.name  //实际调用的是
"println bean.getName()"

采用像直接访问的方式的目的是为了增加代码的可读性,使它更加自然,而在内部,Groovy依然
是在调用setter和getter方法。

闭包的delegate机制

简单来说,delegate机制可以使我们将一个闭包中的执行代码的作用对象设置成任意其他对象。

class Child {
   private String name
}
class Parent {
   Child child = new Child();
   void configChild(Closure c) {
      c.delegate = child
      c.setResolveStrategy Closure.DELEGATE_FIRST
      c()
   }
}

def parent = new Parent()
parent.configChild {
name = "child name"
}

println parent.child.name

在上面的例子中,当调用configChild()方法时,并没有指出name属性是属于Child的,但是它的确是在设置Child的name属性。
事实上光从该方法的调用中,我们根本不知道name是属于哪个对象的,你可能会认为它是属于Parent的。
真实情况是,在默认情况下,name的确被认为是属于Parent的,但是我们在configChild()方法的定义中做了手脚,使其不再访问Parent中的name(Parent也没有name属性),而是Child的name。
在configChild()方法中,我们将该方法接受的闭包的delegate设置成了child,然后将该闭包的ResolveStrategy设置成了DELEGATE_FIRST。这样,在调用configChild()时,所跟闭包中代码被代理到了child上,即这些代码实际上是在child上执行的。
此外,闭包的ResolveStrategy在默认情况下是OWNER_FIRST,即它会先查找闭包的owner(这里即parent),如果owner存在,则在owner上执行闭包中的代码。这里我们将其设置成了DELEGATE_FIRST,即该闭包会首先查找delegate(本例中即child),如果找到,该闭包便会在delegate上执行。

联想gradle中声明的方法

在使用Gradle时,我们并没有像上面的parent.configChild()一样指明方法调用的对象,而是在build.gradle文件中直接调用task(),apply()和configuration()等方法。这是
因为在没有说明调用对象的情况下,Gradle会自动将调用对象设置成当前Project。
比如调用apply()方法和调用project.apply()方法的效果是一样的。查查Gradle的Project文档,你会发现这些方法都是Project类的方法。
对于configurations()方法,该方法实际上会将所跟闭包的delegate设置成ConfigurationContainer,然后在该ConfigurationContainer上执行闭包中的代码。再比如,dependencies()方法,该方法会将所跟闭包的delegate设置成DependencyHandler。

终于到了gradle

自定义Property

Gradle还为我们提供了多种方法来自定义Project的Property。

在build.gradle文件中定义Property

添加一个名为property1的Property:

ext.property1 = "this is property1"

或者采用闭包的形式

ext {
   property2 = "this is property2"
}

定义了Property后,使用这些Property时我们则不需要ext,而是可以直接访问:

task showProperties << {
   println property1
   println property2
}

还可以在执行命令行的时候加属性

task showCommandLieProperties << {
   println property3
}
//以下是cmd中执行命令
gradle -Property3="this is property3" showCommandLieProperties

//通过JVM系统参数定义Property,与java类似,但是前面要约定以“org.gradle.project”为前缀
gradle -D org.gradle.project.property3="this is another property3" showCommandLieProperties

此外还可以通过环境变量来为Gradle设置Property,但是每一个Property都需要以“ORG_GRADLE_PROJECT_”为前缀:

ORG_GRADLE_PROJECT_property3="this is yet another property3"

Gradle 的 Plugin

Gradle最常用的Plugin便是java Plugin了。和其他Plugin一样,java Plugin并没有什么特别的地方,只是向Project中引入了多个Task和Property。当然,java Plugin也有比较与众不同的地方,其中之一便是它在项目中引入了构建生命周期的概念,就像Maven一样。但是,和Maven不同的是,Gradle的项目构建生命周期并不是Gradle的内建机制,而是由Plugin自己引入的。

依赖管理

一个项目总会依赖于第三方,要么是一个第三方类库,要么是自己开发的另一个module
配置Gradle的Repository,就是告诉Gradle在什么地方去获取这些依赖

repositories {
   mavenCentral()
   jCentral()
}

jCentral()是大于mavenCentral()的一个仓库,现在是studio默认的仓库

Gradle对依赖进行分组,允许编译时使用一组依赖,运行时使用另一组依赖。每一组依赖称为一个Configuration,在声明依赖时,我们实际上是在设置不同的Configuration。

要定义一个Configuration,我们可以通过以下方式完成:studio一般不需要设置,应该是有默认的,即为classpath

configurations {
   myDependency
}

通过dependencies()方法向myDependency中加入实际的依赖项:

dependencies {
//下面的myDependency是关键
   myDependency 'org.apache.commons:commons-lang3:3.0'
}
//类似studio中的classpath
dependencies {
   classpath 'com.android.tools.build:gradle:1.3.0'
}
//还有 这里的compile,testCompile
dependencies {
    compile project(':library')
    compile 'com.android.support:recyclerview-v7:22.2.1'
    compile 'com.android.support:design:22.2.1'
    compile 'com.evernote:android-intent:1.0.1'
    testCompile 'junit:junit:4.8.2' 
}

myDependency,classpath,compile,testCompile都是Configuration(一组依赖)。
除了myDependency都不使我们定义的,为啥呢,android Plugin会自动定义compile和testCompile分别用于编译Java源文件和编译Java测试源文件。classpath应该是用于所有,我类推的。
Gradle还允许我们声明对其他Project或者文件系统的依赖。

dependencies {
//library是另一个module的名字
   compile project(':library')
}

对于本地文件系统中的Jar文件,我们可以通过以下方式声明对其的依赖:

dependencies {
   //java
   compile files('spring-core.jar', 'spring-aap.jar')
   compile fileTree(dir: 'deps', include: '*.jar')
   //studio中一般这么写
   compile fileTree(dir: 'libs', include: ['*.jar'])
}

构建多module的project

Gradle为每个build.gradle都会创建一个相应的module领域对象,在编写Gradle脚本时,我们实际上是在操作诸如module这样的Gradle领域对象。在多module的项目中,我们会操作多个module领域对象。Gradle提供了强大的多module构建支持
要创建多module的Gradle项目,我们首先需要在根(Root)Project中加入名为settings.gradle的配置文件,该文件应该包含各个子module(其实就是一个子project)的名称。如setting.gradle中:

include 'library', 'demo'

类似module(子project)的build.gradle,(Root)Project也有自己的build.gradle,在里面通常设置:

allprojects {
    repositories {
        jcenter()
    }
    //通常studio项目没有,咱自己加的
   apply plugin: 'idea'
   task allTask << {
      println project.name
   }
}

allprojects()方法将repositories配置一次性地应用于所有的module(子Project)和root-project本身,当然也包括定义的Task,这个task配置到所有module里面了和root-project。

subprojects()方法用于配置所有的子Project(不包含根Project)

步入巅峰

Gradle本身只是一个架子,真正起作用的是Task和Plugin。

自定义Task

Gradle中的Task要么是由不同的Plugin引入的,要么是我们自己在build.gradle文件中直接创建的。

  • 在build.gradle文件中直接定义
    需要定义的Task类型不多时
    Gradle其实就是groovy代码,所以在build.gradle文件中,我们便可以定义Task类。
class HelloWorldTask extends DefaultTask {
    //@Optional,表示在配置该Task时,message是可选的。
    @Optional
    String message = 'I am jjx'
    //@TaskAction表示该Task要执行的动作,即在调用该Task时,hello()方法将被执行
    @TaskAction
    def hello(){
        println "hello world $message"
    }
}

//hello使用了默认的message值
task hello(type:HelloWorldTask)

//重新设置了message的值
task helloOne(type:HelloWorldTask){
   message ="I am a android developer"
}
  • 在当前工程中定义Task类型
    只能应用在当前module中,没什么卵用,下面是全局可用的
  • 在单独的项目中定义Task类型
    项目中存在大量的自定义Task类型时,在另外的一个gradle文件中定义这些Task,然后再apply到build.gradle文件中。
    可以参考印象笔记的demo:https://github.com/evernote/evernote-sdk-android
    中的:
//这是插件
apply plugin: 'com.android.application'
//这里gradle-quality.gradle就是另外单独定义了task的gradle
apply from: '../build-config/gradle-quality.gradle'
自定义Plugin

与自定义task极其类似,可以类推理解,也是有3中方式定义,只是代码不一样:

apply plugin: DateAndTimePlugin

dateAndTime {
    timeFormat = 'HH:mm:ss.SSS'
    dateFormat = 'MM/dd/yyyy'
}

class DateAndTimePlugin implements Plugin<Project> {
    //该接口定义了一个apply()方法,在该方法中,我们可以操作Project,
    //比如向其中加入Task,定义额外的Property等。
    void apply(Project project) {
        project.extensions.create("dateAndTime", DateAndTimePluginExtension)

        project.task('showTime') << {
            println "Current time is " + new Date().format(project.dateAndTime.timeFormat)
        }

        project.tasks.create('showDate') << {
            println "Current date is " + new Date().format(project.dateAndTime.dateFormat)
        }
    }
}
//每个Gradle的Project都维护了一个ExtenionContainer,
//我们可以通过project.extentions进行访问
//比如读取额外的Property和定义额外的Property等。
//向Project中定义了一个名为dateAndTime的extension
//并向其中加入了2个Property,分别为timeFormat和dateFormat
class DateAndTimePluginExtension {
    String timeFormat = "MM/dd/yyyyHH:mm:ss.SSS"
    String dateFormat = "yyyy-MM-dd"
}

每一个自定义的Plugin都需要实现Plugin<T>接口,除了给Project编写Plugin之外,我们还可以为其他Gradle类编写Plugin。
以上是在build.gradle文件中直接定义Plugin,还可以在当前工程中、单独的项目中创建Plugin,一般情况不需要了解。

怎么样,通过以上学习,再看android studio的gradle代码是不是小case了呢!

下篇有机会一句一句分析studio 项目里常用的gradle代码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • 本篇主要是个人学习gradle的笔记总结 一.开始之前 1. 为什么学习Gradle 采用DSL(Doma...
    zyq_neuq阅读 1,485评论 2 12
  • 这篇文章讲给大家带来gradle打包系列中的高级用法-自己动手编写gradle插件。我们平常在做安卓开发时,都会在...
    呆萌狗和求疵喵阅读 15,969评论 22 80
  • 是什么? 在语法上是基于Groovy语言的(Groovy 是一种基于JVM的敏捷开发语言,可以简单的理解为强类型语...
    千山万水迷了鹿阅读 99,447评论 4 122
  • 昨晚实在无聊,半夜爬起来看BBC的《数学的故事》。当那个长得像Daniel Craig的大叔以极其诡异的手法解决了...
    Pope怯懦懦地阅读 898评论 0 0