Learning Gradle

Posted on Jul 23, 2016

Android Studio作为Android应用开发的官方IDE,默认使用Gradle作为构建工具,所以对于Android应用开发来说,Gradle是必须要掌握的工具。然而现实是,很多Android应用开发人员都不太了解Gradle,并且网上大部分关于Android Gradle的资料都是帮助解决某个具体的配置问题,缺乏系统深入的讲解。本文就来系统且深入的学习Gradle

Java构建工具的发展

Java构建工具最早出现的是Ant。Ant里的每一个任务(target)都可以互相依赖。Ant的最大缺点就是依赖的外部库也要添加到版本控制系统中,因为Ant没有一个机制来把这些外部库文件放在一个中央库里面,结果就是不断的拷贝和粘贴代码。

随后Maven在2004年出现了,Maven引入了标准的项目和路径结构,还有依赖管理,不幸的是自定义的逻辑很难实现,唯一的方法就是引入插件。

随后Ant通过Apache Ivy引入依赖管理来跟上Maven的脚步,Ant和Ivy集成实现了声明式的依赖,比如项目的编译和打包过程。

Gradle的出现满足了很多现在构建工具的需求,Gradle提供了一个DSL(领域特定语言),一个约定优于配置的方法,还有更强大的依赖管理,Gradle使得我们可以抛弃XML的繁琐配置,引入动态语言Groovy来定义你的构建逻辑。

Why Gradle

Android Studio Project Site上对Android Studio为何选用Gradle作为构建工具描述如下:

Gradle is an advanced build system as well as an advanced build toolkit allowing to create custom build logic through plugins. Here are some of its features that made us choose Gradle:

  • Domain Specific Language (DSL) based on Groovy, used to describe and manipulate the build logic
  • Build files are Groovy based and allow mixing of declarative elements through the DSL and using code to manipulate the DSL elements to provide custom logic.
  • Built-in dependency management through Maven and/or Ivy.
  • Very flexible. Allows using best practices but doesn’t force its own way of doing things.
  • Plugins can expose their own DSL and their own API for build files to use.
  • Good Tooling API allowing IDE integration.

DSL,领域特定语言,指不像通用目的语言那样目标范围涵盖一切软件问题,而是专门针对某一特定问题的计算机语言,如init.rc,renderscript等。

理解Groovy

由于Gradle是基于Groovy开发的,要深入理解Gradle,必须先了解Groovy。Groovy概括的说就是把写Java程序变得像写脚本一样简单,写完就可以执行,Groovy内部会将其编译成Java字节码,然后启动虚拟机来执行。Groovy是用于Java虚拟机,具有像PythonRubySmalltalk语言特性的敏捷的动态语言,使用该种语言不必编写过多的代码,同时又具有闭包和动态语言中的其他特性。Groovy使用方式基本与Java代码的使用方式相同,其设计时充分考虑了Java集成,这使Groovy与Java代码的互操作很容易。

Groovy安装

Mac上可以直接通过Homebrew安装Groovy,具体命令如下:

$ brew install groovy

安装完成后可通过如下命令查看:

$ groovy -v
Groovy Version: 2.4.8 JVM: 1.8.0_45 Vendor: Oracle Corporation OS: Mac OS X

Groovy基础语法

  • 可以不同分号结尾。
  • 支持动态类型,即定义变量时可以不指定其类型。
  • 可以使用关键字def定义变量和函数(Groovy推荐)(其实def会改变变量的作用域)。
  • Groovy中的所有事物都是对象。
def var = "Hello Groovy"
println var
println var.class

var = 5
println var
println var.class

输出结果如下:

Hello Groovy
class java.lang.String
5
class java.lang.Integer
  • 单引号中的内容严格对应Java的String,不对$进行转义。
  • 双引号的内容如果有$则会对$表达式先求值(GString)。
  • 三引号可以指示一个多行的字符串,并可以在其中自由的使用单引号和双引号。
def name = 'Jerry'
println 'His name is $name'
println "His name is $name"

def members = """
    'Terry'
    "Larry" """
println "Team member is: " + members

输出结果如下:

His name is $name
His name is Jerry
Team member is:
    'Terry'
    "Larry"
  • 函数定义时返回值和函数参数也可以不指定类型。
  • 未指定返回类型的函数定义必须使用def。
  • 可以不使用return xxx来设置函数返回值,函数最后一行代码的执行结果被设置成返回值,如果定义时指明了类型则必须返回正确的数据类型。
  • 函数调用可以不加括号,构造函数和无参函数除外。
def getValue(name) {// def is must
    name + "'s value is 10"
}
value = getValue "Terry"
println value

结果如下:

Terry's value is 10
  • Java原始数据类型在Groovy中为其对应的包装类型。
  • 类不支持default作用域,且默认作用域为public,如果需要public修饰符,则不用写它。
  • 自动提供足够使用的构造函数(一个无参和带一个Map参数的构造函数,足够)。
  • Groovy动态的为每一个字段都会自动生成getter和setter,并且我们可以通过像访问字段本身一样调用getter和setter。
  • Groovy所有的对象都有一个元类metaClass,我们可以通过metaClass属性访问该元类。通过元类,可以为这个对象增加属性和方法(在java中不可想象)!
  • 常用的集合类有ListMapRange
class Person {
    int id
    String name

    void setId(id) {
        println "setId($id)"
        this.id = id
    }

    String toString() {
        "id=$id, name=$name"
    }
}
merry = new Person()
merry.id = 1    // call merry.setId(1)
merry.setName "Merry"
println merry
println merry.getId().class
jerry = new Person(id: 2, name: "Jerry")
println jerry

运行结果如下:

setId(1)
id=1, name=Merry
class java.lang.Integer
setId(2)
id=2, name=Jerry

下面的例子展示通过metaClass向String对象中动态添加属性和方法:

def msg = "Hello Groovy"
msg.metaClass.upper = { delegate.toUpperCase() }
msg.metaClass.lower = msg.toLowerCase()
println msg.upper()
println msg.lower

运行结果如下:

HELLO GROOVY
hello groovy

Groovy动态性

Groovy动态性示例如下:

class Dog {
    def bark() {
       println 'woof!'
    }
    def sit() {
       println 'sitting!'
    }
    def jump() {
       println 'boing!'
    }
}

def dog = new Dog()
def acts = ['sit','jump','bark']
acts.each {
    dog."${it}"()
}

运行结果如下:

sitting!
boing!
woof!

另一个动态性的例子:

class LightOn
{
    def doing()
    {
       println 'Ligth turning on...'
    }
}

class LightOff
{
    def doing()
    {
       println 'Ligth turning off...'
    }
}

class Switch
{
    def control(action)
    {
       action."doing"()
    }
}
def sh = new Switch()
sh.control(new LightOn())

运行结果如下:

Ligth turning on...

Groovy List

  • List变量由[]定义,其元素可以是任意对象,底层对应Java的List接口。
  • 直接通过索引存取,而且不用担心索引越界,当索引超过当前列表长度,List自动往该索引添加元素。
tmp = ["Jerry", 19 , true]
println "tmp[1] = " + tmp[1]
println tmp[5] == null
tmp[10] = 3.14
println "The size is " + tmp.size
println tmp

运行结果如下:

tmp[1] = 19
true
The size is 11
[Jerry, 19, true, null, null, null, null, null, null, null, 3.14]

Groovy Map

  • Map变量由[:]定义,冒号左边是key,右边是value,key必须是字符串,value可以是任何对象。另外key可以用引号包起来,也可以不用。
  • Map中元素的存取支持多种方法。
def score = "mark"
// id and name are treated as String
// "$score" is also correct
tmp = [id: 1, name: "Jerry", (score): 92]
println tmp
println "id: " + tmp.id
println "name: " + tmp["name"]
tmp.height = 183
println "height: " + tmp.height

运行结果如下:

[id:1, name:Jerry, mark:92]
id: 1
name: Jerry
height: 183

Groovy Range

  • Range是Groovy对List的扩展,由begin值 + .. + end值定义。
  • 不包含end值时使用<。
tmp = 1..5
println tmp
println tmp.from
println tmp.to
tmpWithoutEnd = 1..<5
println tmpWithoutEnd.step(2)
println ""
println ""
println tmpWithoutEnd


tmp = 1.0f..5.0f
println tmp

运行结果如下:

[1, 2, 3, 4, 5]
1
5
[1, 3]


[1, 2, 3, 4]
[1.0, 2.0, 3.0, 4.0, 5.0]

Groovy文档

  • Groovy的API文档位于http://www.groovy-lang.org/api.html。
  • 以Range为例,从getter方法我们知道Range有fromto属性,尽管文档中并没有说明。
T	getFrom()
The lower value in the range.
T	getTo()
The upper value in the range.

Groovy闭包

  • 英文叫Closure,是Groovy中非常重要的一个数据类型或者说一种概念了。
  • 闭包,是一种数据类型,它代表了一段可执行代码或方法指针。定义格式为:
    • def 闭包对象 = { parameters -> code }
    • def 闭包对象 = { code } // 参数个数少于2个时可以省略->符号
    • `def 闭包对象 = reference.&methodName
  • 闭包的调用方式:
    • 闭包对象.call(参数)
    • 闭包对象(参数)
  • 闭包只有一个参数时,可省略书写参数,在闭包内使用it变量引用参数。
  • 闭包可以作为函数返回值,函数参数,可以引用闭包外部定义的变量,可以实现接口方法。
def person = { id, name ->
    println "id=$id, name=$name"
}
person(1, "Jerry")
person.call(2, "Larry")
person 3, "Merry"

Closure resume = { "resume" }
println resume()

def hello = "Hello"
def length = hello.&length
println "The length is " + length()

def greeting = { "$hello, $it" }
// equals: greeting = { it -> "Hello, $it" }
println greeting('Groovy')

def exit = { -> "exit" }
// exit(1) <= wrong!!

def members = ["Jerry", "Larry", "Merry"]
members.each {
    println "Hello $it"
}

def welcome(name) {
    return {
        println "Welcome $name"
    }
}
println welcome("Terry")

运行结果如下:

id=1, name=Jerry
id=2, name=Larry
id=3, name=Merry
resume
The length is 5
Hello, Groovy
Hello Jerry
Hello Larry
Hello Merry
closure$_welcome_closure6@73a1e9a9

如何确定闭包的参数?查看API文档。

Groovy DSL

  • Command Chain: 链式调用既可以省略圆括号,又可以省略”.”号。
  • 闭包作为函数调用的最后一个参数时,可以拿到圆括号外面。
// 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 { }

一个例子:

def name(name) {
    println "name: $name"
    return this
}
def age(age) {
    println "age: $age"
    return this
}
def action(String tips, Closure c) {
    println "begin to $tips"
    c()
    println "end"
    return this
}

name "Jerry" age 18
// name("Jerry").age(18)

name "Herry"
age 22
action("eat") {
    println "eating..."
}

运行结果如下:

name: Jerry
age: 18
name: Herry
age: 22
begin to eat
eating...
end

Groovy脚本

Groovy脚本是什么?我们通过一个例子来看一下。

// variables.groovy

def x = 1 // or int x = 1

def printx() {
    println x
}

printx() // failed

运行结果如下:

Caught: groovy.lang.MissingPropertyException: No such property: x for class: variables
groovy.lang.MissingPropertyException: No such property: x for class: variables
	at variables.printx(variables.groovy:5)
	at variables.run(variables.groovy:8)

为何会出现找不到属性x?我们来看看反编译groovy运行时生成的.class文件。使用如下命令在-d指定的目录生成.class文件,然后使用JD-GUI查看:

groovyc –d classes variables.groovy

variables.class 可以看到:

  • XXX.groovy被转换成XXX类,它从Script类派生。
  • 每一个脚本都会生成一个static main函数。这样,当我们groovy XXX.groovy的时候,其实就是用调用虚拟机去执行这个main函数。
  • 如果脚本定义了函数,则函数会被定义在XXX类中。
  • 脚本中的其他代码都会放到run函数中。

所以变量x是在是在run函数中定义的局部变量,当然无法在printx()函数的访问。那要如何才能实现printx()函数访问变量x呢?有两种方式可以实现。 第一种方式:

x = 1 // replace def x = 1 or int x = 1

def printx() {
    println x
}

printx()

对应的.class文件反编译代码: 另一种方式:

import groovy.transform.Field;

@Field x = 1 // <= def x = 1 or int x = 1

def printx() {
    println x
}

printx()

对应的.class文件反编译代码:

Gradle介绍

Gradle被认为是Java世界构建工具的一次飞跃,它提供:

  • 一个非常灵活通用的构建工具。
  • 对多项目构建提供强大支持。
  • 强大的依赖管理机制。
  • 完美支持已有的Maven和Ivy仓库。
  • 支持依赖传递管理。
  • 脚本编写基于Groovy。
  • 丰富的描述构建的领域模型。

更多Gradle特性概述,可以访问Gradle概述

Why Groovy

Gradle官方网站上对为何选用Groovy来开发的原因描述如下:

We think the advantages of an internal DSL (based on a dynamic language) over XML are tremendous when used in build scripts. There are a couple of dynamic languages out there. Why Groovy? The answer lies in the context Gradle is operating in. Although Gradle is a general purpose build tool at its core, its main focus are Java projects. In such projects the team members will be very familiar with Java. We think a build should be as transparent as possible to all team members.

In that case, you might argue why we don’t just use Java as the language for build scripts. We think this is a valid question. It would have the highest transparency for your team and the lowest learning curve, but because of the limitations of Java, such a build language would not be as nice, expressive and powerful as it could be. [1] Languages like Python, Groovy or Ruby do a much better job here. We have chosen Groovy as it offers by far the greatest transparency for Java people. Its base syntax is the same as Java’s as well as its type system, its package structure and other things. Groovy provides much more on top of that, but with the common foundation of Java.

For Java developers with Python or Ruby knowledge or the desire to learn them, the above arguments don’t apply. The Gradle design is well-suited for creating another build script engine in JRuby or Jython. It just doesn’t have the highest priority for us at the moment. We happily support any community effort to create additional build script engines.

Gradle基本概念

Gradle脚本是配置脚本,脚本执行时会配置特定的对象,这些对象被称为脚本的delegate对象。脚本中可以使用delegate对象的属性和方法。所有的Gradle脚本实现了org.gradle.api.Script接口,所以脚本中可以直接使用Script接口定义的属性和方法,Script接口中找不到的属性或方法将被转交给delegate对象。同时,Gradle脚本也是Groovy脚本,同样可以包含Groovy脚本允许的元素,方法、类定义等。 gradle-delegates

Gradle生命周期

Gradle执行构建时有它自己的生命周期,概括来说可以分为3个阶段: 第一是初始化阶段。初始化阶段Gradle会创建一个Gradle对象、Settings对象和Root Project对象,然后在项目根目录寻找并执行settings.gradle文件来配置Settings对象,并生成项目的Project树。

第二是配置阶段。配置阶段Gradle会执行特定的构建脚本,默认是各Project目录下名为build.gradle的文件,以配置对应的Project。配置阶段结束时,Gradle内部会生成整个项目的有向无循环Task图。

第三是执行阶段。执行阶段Gradle依据命令行传入的task名字在Task图中找出此task的所有依赖链,并从各依赖链起点开始,沿着依赖链依次执行Task,最终得到编译产物。 gradle-build-phasesM

Gradle用户手册中关于Gradle生命周期的描述如下: gradle-lifecycle

Gradle生命周期的例子如下:

// settings.gradle
println 'This is executed during the initialization phase.'

// build.gradle in root directory
println 'This is executed during the configuration phase.'

task configured {
    println 'This is also executed during the configuration phase.'
}
task test {
    doLast {
        println 'This is executed during the execution phase.'
    }
}
task testBoth {
    doFirst {
      println 'This is executed first during the execution phase.'
    }
    doLast {
      println 'This is executed last during the execution phase.'
    }
    println 'This is executed during the configuration phase as well.'
}

执行结果如下:

This is executed during the initialization phase.
This is executed during the configuration phase.
This is also executed during the configuration phase.
This is executed during the configuration phase as well.

Gradle生命周期Hook

Gradle提供了丰富的Hook机制以获知Gradle生命周期各个阶段各类事件的发生。常用的Hook可以通过Settings对象,ProjectTask对象来设置,设置方式包括配置闭包方式和设置监听器方式。 gradle-lifecycle-phases-hook

以项目评估结束事件为例说明:

// build.gradle in root directory
afterEvaluate {
    println "Project $name has been evaluated."
}

gradle.afterProject {
    println "Project $it.name is evaluated."
}

gradle.addProjectEvaluationListener(new ProjectEvaluationListener() {
    void afterEvaluate(Project project, ProjectState state) {
        println "Project $project.name was evaluated."
    }
    void beforeEvaluate(Project project) {
    }
})

执行结果如下:

Project tutorial was evaluated.
Project tutorial is evaluated.
Project tutorial has been evaluated.

Gradle核心组件

Gradle的核心组件包括GradleSettingsProjectTaskDependencyPlugin等,每个核心组件都对应很多有用的属性、方法和脚本块。

Gradle组件

Gradle组件代表了一次Gradle构建,通过Gradle组件能够获取(或设置)本次构建的一些全局信息,包括gradleVersiontaskGraph,所有用到的plugins及添加各种Hook等。

Settings组件

Settings实例与settings.gradle文件存在一一对应关系,Gradle执行时会使用settings.gradle来配置对应的Settings实例。通常,在settings.gradle脚本中会通过include(String[])方法来添加对多项目的支持。Root Project会在Settings对象被创建时自动添加。另外,Settings组件还支持动态属性,除了对象接口本身的属性,还包含如下可用的属性:

  • setting.gradle同级的gradle.properites文件定义的属性。
  • 用户Gradle目录里gradle.properties定义的属性。
  • 通过命令行-P参数定义的属性。

示例如下:

// gradle.properties in project root directory
componentized=true

// settings.gradle
if (componentized) {
    include ':components'
}

执行结果如下:

Project tutorial was evaluated.
Project tutorial is evaluated.
Project tutorial has been evaluated.
Project components was evaluated.
Project components is evaluated.

Project组件

Project组件是编译脚本与Gradle交互的主API接口,通过Project接口能访问Gradle所有功能。Gradle运行时使用build.gradle配置对应的project对象。一个Project本质上一些列Task的集合,每一个Task执行一定的任务(类编译,生成javadoc等)。Project配置通常包括仓库,依赖,产物,分组配置,插件的管理。所有Project将都将被加入到Project层次树中,Project全路径是其在层次树中的唯一标识。Project配置还可以引入插件,插件可以使得Project配置更加模块化,并可重用。Project组件还支持5大范围的动态属性和5大范围的动态方法。

5大范围动态属性 project-5-property-scopes

5大范围动态方法 project-5-method-scopes

Ext属性示例:

// build.gradle in root directory
ext {
    gradleVersion = "3.4"
    isSnapshot = true
}
ext.javaVersion = "1.7"

task addProperty(dependsOn: clean) {
    project.ext.prop3 = true
}
if (isSnapshot) {
    println "GradleVersion: $gradleVersion"
    println "JavaVersion: $javaVersion"
    println "Prop3: ${prop3}"
}

// build.gradle in components directory
println "GradleVersion in components: $gradleVersion"
println "JavaVersion in components: ${rootProject.ext.javaVersion}"

执行结果如下:

GradleVersion: 3.4
JavaVersion: 1.7
Prop3: true
GradleVersion in components: 3.4
JavaVersion in components: 1.7

依赖管理

Gradle的依赖管理主要包括2大块:

  • 解决依赖。依赖指使得项目能够正常编译或运行的输入组件。
    • 依赖配置:包括项目内依赖和外部依赖,并以配置分组。
    • 仓库配置:Gradle会在配置的仓库地址查找外部依赖。
  • 发布产物。产物指项目编译生成的或需要上传到中心的仓库的输出产物。
    • 配置发布仓库。通常插件会定义好项目的产物,我们不必关心,但必须告诉Gradle将产物发布到何处,MavenCentral,Ivy仓库,JCenter等。

Gradle有6大类型依赖,如下图,其中比较常用的包括External module dependencyProject dependencyFile dependencydependency-types

依赖管理示例:

// In this section you declare where to find the dependencies of your project
repositories {
    jcenter()
    jcenter {
        url "http://jcenter.bintray.com/"
    }
    mavenCentral()
    maven {
        url "http://repo.company.com/maven2"
    }
    ivy {
        url "../local-repo"
    }
}

dependencies {
    compile 'com.google.guava:guava:20.0'
    compile project(':components')
    compile fileTree(dir: 'libs', include: '*.jar')
    testCompile group: 'junit', name: 'junit', version: '4.+'
}

Task组件

Gradle task represents some atomic piece of work which a build performs。Task可以使用task关键字定义,也可以使用TaskContainer.create()来定义。一个Task可以依赖于另一个Task,Gradle支持使用must run aftershould run after直接控制2个Task的执行顺序。预定义的Task可以被替换。 另外Task还支持4大范围动态属性和通过Convention对象支持动态方法。

4大范围动态属性 task-4-property-scopes

Task使用示例:

// build.gradle in root project
task hello1 {
    doLast {
        println "Hello1 Task"
    }
}
task hello2 << {
    ext.prop2 = "prop2"
    println "Hello2 Task"
}
hello2.dependsOn hello1
task(hello3, dependsOn: hello2) {
    doFirst {
        println "Prop2: ${hello2.ext.prop2}"
        println "Hello3 Task"
    }
}
tasks.create('hello4').dependsOn('hello3')
hello4.doLast {
    println "Hello4 Task"
}

运行结果如下:

:hello1
Hello1 Task
:hello2
Hello2 Task
:hello3
Prop2: prop2
Hello3 Task
:hello4
Hello4 Task

Plugin组件

Gradle插件就是将很多可在多个不同项目或构建中重用的编译逻辑(属性、方法和Task等)打包起来。可以使用不同的语言来实现插件,如Groovy,Java,Scala,插件最终被编译成字节码。插件源码可位于3类地方:

  • Build script:插件自动被编译并被include到classpath中,build script外不可用。
  • buildSrc project:buildSrc子项目自动被编译并被include到classpath中,整个项目可用,其他项目不可用。
  • Standalone project:可以发布jar或发布到公共仓库,其他项目可用。

Gradle Wrapper目录结构

Gradle Wrapper可以使得每个项目拥有各自的Gradle版本而不互相影响。当前项目中Gradle相关文件说明如下: gradle-wrapper-in-project

用户主目录下Gradle相关文件说明如下: gradle-wrapper-in-home

Android Plugin for Gradle

Android Plugin for Gradle就是通过Gradle插件机制方式实现Gradle在Android平台的扩展,详细信息请参考Gradle Plugin User GuideAndroid Gradle DSL Reference


参考