前言
学习gradle这个想法在我开始使用AndroidStudio之后就有了,基本上每个开始使用AndroidStudio的人都会被它折磨一段时间,各种各样的build failed,但是由于种种原因没有深入学习,每次都是出来问题再去网上查一下。这段时间终于有时间把gradle拿出来好好学习一下,其实感觉过程是很痛苦的,主要是gradle中涉及的api实在是太多了,以及各种DSL,导致整个Grade的学习一度陷入僵局,直到现在也不敢说对gradle有太深入的研究,也算是对gradle做一个介绍
Groovy基础
说实话对于Groovy我没有太多了解,我感觉前期了解个大概就好了,如果想要学习的细致一点,这里推荐一篇文章,我上面的很多内容也是来自这里Groovy脚本基础全攻略
Gradle是一个构建工具,就像Ant使用xml进行配置一样,Gradle使用Groovy语言进行配置,Groovy源自Java,但是Groovy抛弃了java中很多文法,Groovy更像一种脚本语言。执行Groovy脚本时,Groovy会先将其编译成Java类字节码,然后通过Jvm来执行这个Java类。 作为一种动态语言,Groovy的语法与我们平时用的java还是有很大不同的,不过习惯之后其实和java还是很类似的(这句话说的还是很违心的 - -!)
变量定义
Groovy作为动态语言,可以在变量定义时不指定类型,直接使用def关键字
def x = 1
//当然也可以在定义时指定变量类型
def int y = 1
//省略def也是可以的,但一般不推荐
x = 1
这里定义了一个int型的变量,当然def这个关键字也不是必须的,可以直接写成
x = 1
也是同样定义了一个int型的变量。当然建议大家还是把def加上,以免混乱
- 还有一点大家可能也注意到了,groovy中可以不使用”;”作为一行代码的结束符
- groovy的变量定义没有java中的类似public等修饰符,这是因为groovy中默认的修饰符就是public的,可以添加private等修饰符
字符串
Groovy支持多种字符串形式,最常用的是单引号和双引号
def name = 'Groovy!'
def body1 = 'Test ${name}'
def body2 = "Test ${name}"
assert name == 'Groovy!'
//$表示占位符,但是单引号字符串不支持占位符
assert body1 == 'Test ${name}'
//使用双引号字符串可以支持占位符
assert body2 == 'Test Groovy!'
在groovy中插值占位符我们可以用或者
来标示,用于一般替代字串或者表达式,
主要用于A.B的形式,除了上面两种字符串,groovy还支持三引号
def aMultilineString = '''line one
line two
line three'''
新的行被转换为“\n”,其他所有的空白字符都被完整的按照文本原样保留
闭包
Groovy的闭包(closure)是一个非常重要的概念,闭包是可以用作方法参数的代码块,Groovy的闭包更象是一个代码块或者方法指针,代码在某处被定义然后在其后的调用处执行。闭包在gradle中使用也是很多的,弄懂闭包对于gradle 的学习很重要
定义一个闭包
{ [closureParameters -> ] statements }
//[closureparameters ->是可选的逗号分隔的参数列表,参数类似于方法的参数列表,这些参数可以是类型化或非类型化的。
如下给出几个有效的闭包定义例子:
//最基本的闭包
{ item++ }
//使用->将参数与代码分离
{ -> item++ }
//使用隐含参数it(后面有介绍)
{ println it }
//使用明确的参数it替代
{ it -> println it }
//使用显示的名为参数
{ name -> println name }
//接受两个参数的闭包
{ String x, int y ->
println "hey ${x} the value is ${y}"
}
//包含一个参数多个语句的闭包
{ reader ->
def line = reader.readLine()
line.trim()
}
其实groovy和java没有本质上的区别,最大的区别在书写形式上,在内容上的区别如下
groovy会自动导入部分java常用包
-
对于重载方法,java会在编译时选择相应方法而groovy实在运行是,具体如下
int method(String arg) { return 1; } int method(Object arg) { return 2; } Object o = "Object"; int result = method(o);
在java中结果是assertEquals(2, result);
在groovy中结果是assertEquals(1, result);
- groovy中{}是用于闭包的,所以数组初始化只能使用int[] array = [1,2,3]
的形式 - java中变量定义不写修饰默认就是package-private的,groovy默认就是public,要定义package-private必须使用@PackageScope 注解,比如@PackageScope String name
更多更详细的区别,见Differences with Java
groovy中对java的api有一些扩展,groovy主要对java的io文件操作以及集合类api进行了拓展,详情见The Groovy Development Kit
Gradle
Gradle命令行配置
不像很多其他的工具,Gradle提供了一个简单的东西让我们能够快速使用命令行,那就是GradleWrapper,在一个拥有GradleWapper的工程里,我们不用配置任何信息,就能直接使用Gradle的命令:
./gradlew <task> //在Linux和Mac上
gradlew <task> //在windows上
当然代价就是Gradle会去联网下载对应的Gradle版本,如果网速如果慢的的话就GG了,当然Wrapper还有其他好处,它可以让工程统一构建版本,使用统一版本的构建工具来运行
由于我的龟一样的网速,这里我通过配置Gradle命令行来运行gradle,把gradle版本定死了,首先在系统变量中配置GRAGLE_HOME
***\Android Studio\gradle\gradle-2.14.1
这里我们直接使用了AndroidStudio下的gradle插件,然后在Path中配置
%GRADLE_HOME%\bin\
打开命令行输入gradle tasks,可以看到一系列gradle下可执行的任务。说明gradle配置成功了
Gradle基础
Gradle脚本是配置脚本,脚本执行时,他配置了一个特定类型的对象,比如一个build脚本执行时,会生成一个Project 的对象,这个对象被称作脚本的委托对象
脚本类型 | 委托对象 | 说明 |
---|---|---|
Build script | Project | 可以通过这个对象去获取配置文件中的相关信息 |
Init script | Gradle | gradle构建过程有且仅有一个gradle对象,可通过Project.getGradle()获得 |
Settings script | Settings | 在配置过程中,通过这个文件来确定构建的模块 |
除了上面几个委托对象,Gradle中还有有一个重要的对象:
Task task是gradle中真正可执行的对象,grade的构建过程是由很多task来完成的,每个task都属于一个project,我们的编译打包过程其实就是在执行task
Task
task可以直接通过gradle命令来执行,上面使用gradle tasks
看到的一系列task就是gradle已经预先提供好的任务,接下来我们同过自己写一个简单的task来更加清楚的认识一下task
新建一个build.gradle文件
task hello << {
println "hello world!"
}
运行gradle tasks,可以看到最下面多了几行
Other tasks
-----------
hello
这便是我们上面定义的hello,可以直接通过gradle tasks
来运行
>gradle hello
:hello
hello world!
这里我们的task只是简单的在命令行打印了一句话而已,但是task其实可以做更多的操作,我们的工程的构建过程其实也就是在执行task而已,只是task做了更多的工作,同时还依赖了其他更多的task
定义一个Task
task的定义很简单,使用task关键字就可以定义一个task
task myTask
这就是一个简单的定义,这个task可以运行,但是什么都没有做,我们可以对myTask对象进行操作,把任务内容加进去
myTask.doLast {
println 'Hello MyTask'
}
当然我们也可以直接使用task myTask { configure closure }
这种方式进行定义,具体如下
task myTask{
doLast{
println 'Hello MyTask'
}
}
这样的写法和上面表达了同样的意思,然而gradle支持我们简写成下面这种形式
task myTask << {
println 'Hello MyTask'
}
Task间的依赖
task之间可以有依赖关系,这个依赖关系主要通过3个关键字来完成,分别是dependsOn
,shouldRunAfter
,finalizedBy
-
dependsOn
是依赖关系A dependsOn B,A执行必须要B任务先执行 -
finalizedBy
紧接着完成 A dependsOn B,A执行之后 B就要接着执行 -
shouldRunAfter
约束性没有那么强, A shouldRunAfter B,A在B之后执行,但不一定B执行完了A就一定执行, 但是A一定会在B之后执行
这三个关键字的用法都是一样的,只是意义不同, 我们这里借dependsOn
介绍一下依赖关键字的用法
build.gradle
task hello {
doLast {
println 'Hello world!'
}
}
task intro(dependsOn: hello) {
doLast {
println "I'm Gradle"
}
}
执行结果如下
> gradle -q intro
Hello world!
I'm Gradle
-q
是屏蔽日志信息,只显示最终结果,从结果中可以看到,我们执行了intro,但是intro依赖了hello,因此hello在intro前执行了,这种依赖关系还可以使用属性来定义
build.gradle
task intro() {
dependsOn 'hello'
doLast {
println "I'm Gradle"
}
}
或者直接使用task对象来声明依赖关系
task hello {
doLast {
println 'Hello world!'
}
}
task intro() {
doLast { println "I'm Gradle" }}intro.dependsOn hello
这三种方法都可以用来声明task之间的依赖关系
Task Type
我们知道Gradle使用了groovy,而groovy又是基于java的,所以Gradle的很多行为都可以用类似java的形式来说明,task本质上其实就是一种特殊的类,上面我们定义的task都是继承自DefaultTask
这个Task基类的,除了这个基类Gradle还提供了很多扩展类,用于实现很多扩展功能,我们也可以直接基础这些类,在Task定义过程中我们可以让我们的task直接继承自这些类,这个过程通过task type来实现。
具体实现方法如下:
task myTask(type: SomeType)
task myTask(type: SomeType) { configure closure }
官方已经定义了很多种Tasktype,更具体可以参考官方文档Task types,这里我们使用Copy这个Type来进行介绍
Gradle生命周期
Gradle是基于依赖的编程语言,我们可以定义task以及task之间的依赖,Gradle来保证task按照我们给定的依赖顺序来执行,为此Gradle需要在task执行之前生成并维护一个task的DAG。Gradle在执行过程中会经历三个生命周期:
Initialzation
- Grade支持多项目构建,在这个时期,Gradle需要确定哪些project需要进行build,并为每个需要进行build的project创建一个Project对象
Configuration
- 在Configuration时期,每个Project对应的build.gradle文件都会被解析,相应的属性都会加入到对应的Project对象中
Execution
- Gradle中Task的执行时期,Gradle保证每个task都能按照指定的顺序执行
Initialzation
Gradle支持单项目构建和多项目构建(Multi-project builds),构建的类型在Initialzation时期就已经决定了,在Initialzation时期,Gradle会去寻找和解析settings.gradle文件。Gradle寻找settings文件的顺序如下:
- 在命令执行的当前目录去找
- 如果没有找到,去上级目录中找
- 如果还没有找到,Gradle认为这是一个单项目构建
- 如果寻找到settings文件,Gradle会检查当前项目是否在settings定义的多项目层级中(即是否在include声明过),如果没有,同样认为当前项目是一个单项目构建
在多项目构建中,一般的文件结构为一个树形,一个root project,一个或多个subproject,subproject也可能会有subproject,如果根据settings确定为一个多项目构建时,首先初始化的是根节点,然后是settings中配置的各个子节点(这一时期只是生成project对象,并不会对project对象进行和配置)
Configuration
在Configuration时期,每一个subproject对象都会得到配置,每个subproject的配置过程相对独立,但是我们可以通过root project来实现为每个subproject配置统一的属性,这一过程通过Configuration injection来实现。 实现ConfigurationInjection在Gradle中主要通过几个DSL:project、allprojects和subprojects,这里我们借官方文档的一个小例子做一下说明,有兴趣也可以去研究下官方原版
Project目录如下
water/
build.gradle
settings.gradle
bluewhale/
krill/
settings.gradle
include 'bluewhale', 'krill'
这个和我们之前看到的类似include ':bluewhale',':krill'
是一样的,只是省略了冒号
build.gradle
Closure cl = { task -> println "I'm $task.project.name" }
task('hello').doLast(cl)
project(':bluewhale') {
task('hello').doLast(cl)
}
命令行执行gradle -q hello
,结果如下
> gradle -q hello
I'm water
I'm bluewhale
这里我们做了什么?首先定义了一个闭包,打印task执行时的项目名,然后在当前build.gradle中定义了一个task"Hello",并把闭包传入task的任务中,同时为bluewhale的gradle添加了同样一个task。
ConfigurationInjection使得我们可以在root project中直接通过代码配置subproject中的属性(不仅仅只是task),allprojects和subproject也是类似的,不同的是allproject可以为所有project生成相同配置,而subproject会剔除root project,具体看例子
在water中使用如下build.gradle
allprojects {
task hello {
doLast { task ->
println "I'm $task.project.name"
}
}
}
执行结果如下
> gradle -q hello
I'm water
I'm bluewhale
I'm krill
而使用subproject
subprojects {
task hello {
doLast { task ->
println "I'm $task.project.name"
}
}
}
结果会是
> gradle -q hello
I'm bluewhale
I'm krill
可以看到使用subproject时,Gradle会把root project剔除在外,只针对所有subproject进行配置
Gradle task依赖图的管理
上面说道,我们在执行一个Gradle的task时,Gradle会分析整个project,把所有相关联的任务全部添加到一个taskGraph中,当到了excution时期,就会安装taskGraph的顺序来执行。那么我们如何才能动态的根据情况去插入或改变部分task来达到我们的要求。
就比如我们想在java的编译打包的任务中,添加一个生产log文件的需求应该如何实现呢?Gradle为我们提供了很多生命周期相关的方法:
- project.afterEvaluate 在指定的project对象被配置之后,即build.gradle文件已经解析完成
- gradle.afterProject 在所有project对象被配置之后
- tasks.whenTaskAdded 当Task添加到taskGraph中时执行
- gradle.taskGraph.beforeTask 在Task执行之前taskGraph已经构建好,下同
- gradle.taskGraph.afterTask
这时候如果要实现上面的需求我们就可以这样
tasks.whenTaskAdded{task->
if (task.name.equals('jar')) {
task.dependsOn logTask
}
}
Gradle依赖管理
在gradle中依赖管理分为两种,一种是我们的项目使用别人提供的依赖,Gradle可以帮我们快速的找到 这些libraries,无论他们是在本地还是在远程仓库里,另一种是我们的项目本身就是一个library,我们需要生成一个本地的jar,或则上传到远程仓库。
我们先看一段在AndroidProject中声明依赖的代码,首先是在整个工程的build.gradle中
allprojects {
repositories {
jcenter()
}
}
前面我们就已经提到过,allprojects可以为所有项目添加配置,这里就是声明使用jcenter的远程仓库
app module中的build.gradle
apply plugin: 'com.android.application'
dependencies {
testCompile 'junit:junit:4.12'
}
这种类似的代码我们经常看到,我们知道testCompile是测试时编译,类似的还有compile,debugCompile,然而这并不是由Gradle提供的,而是由上面com.android.application
插件提供的,如果没有使用这个插件,运行会报错。
dependencies在Gradle被归类为configurations,而上面的testCompile其实本质上就是一个配置,并没有做什么,android在运行测试时会去获取配置,然后编译相关联的library,这才是testCompile真正的用处。
为了大家能更深入的理解dependency就是配置,我在在这里举一个例子
repositories{
jcenter()
}
configurations{
myCompile {
description = 'compile
classpath' transitive = true
}
}
dependencies{
myCompile 'junit:junit:4.12'
}
task copyAllDependencies(type: Copy) {
from configurations.myCompile
into 'allLibs'
}
然后执行
>gradle cAD
:copyAllDependencies
可以看到当前目录下生成了一个allLibs文件夹
allLibs 的目录
2016/11/24 14:09 <DIR> .
2016/11/24 14:09 <DIR> ..
2016/11/24 14:09 45,024 hamcrest-core-1.3.jar
2016/11/24 14:09 314,932 junit-4.12.jar
junit库已经被拷贝进去了,同时还有它依赖的hamcrest库,这里例子中,myComplie是我们自己定义的,然后我们用它添加依赖,最后拷贝这个依赖,最后成功从远程仓库得到了jar包,这里只是给大家一个直观的印象,dependencies就是配置。
Gradle中依赖主要又两种写法,String记法和Map记法
dependencies {
compile 'junit:junit:4.12'
compile group: 'junit', name: 'junit', version: '4.12'
}
使用String记法在添加远程依赖时,我们只能设置一部分的属性,而使用Map记法我们可以定义所以属性
compile group: 'junit', name: 'junit', version: '4.12', transitive: true
但是两种记法我们都可以用一个闭包来指定所有属性
compile('junit:junit:4.12') {
transitive = true
}
compile(group: 'junit', name: 'junit', version: '4.12') {
transitive = true
}
上面的例子中我们使用了很多次transitive
这条属性,Gradle依赖有个特点,TransitiveDependency
,字面上的意思,依赖可以传递,我们依赖的library可能还依赖着其他的libraries,transitive = true
就是开启依赖传递,这样gradle就会自动下载library中依赖的其他library。类似的属性还有一些,这里我们也会介绍一些,具体的大家可以去查询官方文档,我们看下面这个官方给出的例子
apply plugin: 'java' //so that I can declare 'compile' dependencies
dependencies {
compile('org.hibernate:hibernate:3.1') {
//in case of versions conflict '3.1' version of hibernate wins:
force = true
//excluding a particular transitive dependency:
exclude module: 'cglib' //by artifact name
exclude group: 'org.jmock' //by group
exclude group: 'org.unwanted', module: 'iAmBuggy' //by both name and group
//disabling all transitive dependencies of this dependency
transitive = false
}
}
依赖冲突
Gradle对于依赖版本的冲突一般有两种解决方法
- 当版本冲突时自动使用最新的一个版本,这是gradle的默认方法,一般是也不会出现问题,除非版本不是向下兼容的
- 当我们显式选择了多个版本的依赖,这时会导致build failure(如何显示的选择依赖的版本后面详细讲解)
除了上面两种gradle默认提供的冲突解决,gradle也允许我们自己来主动解决冲突,我们知道依赖本质上就是configurations,我们可以在通过改变全局的配置来解决冲突
apply plugin: 'java' //so that there are some configurations
configurations.all {
resolutionStrategy {
// 如果出现版本冲突就是构建失败
failOnVersionConflict()
// 优先使用编译体系内的moudule而非外部module
preferProjectModules()
// 强行使用对应版本的依赖
force 'asm:asm-all:3.3.1', 'commons-io:commons-io:1.4'
// 替换指定过的依赖版本
forcedModules = ['asm:asm-all:3.3.1']
// 添加依赖替换规则
dependencySubstitution {
substitute module('org.gradle:api') with project(':api')
substitute project(':util') with module('org.gradle:util:3.0')
}
// cache dynamic versions for 10 minutes
cacheDynamicVersionsFor 10*60, 'seconds'
// don't cache changing modules at all
cacheChangingModulesFor 0, 'seconds'
}
}
除了使用上面,通过改变全局配置来解决冲突,我们还可以在添加依赖的时候来改变出现冲突时的决策
apply plugin: 'java'
dependencies {
compile('org.hibernate:hibernate:3.1') {
//如果出现冲突,gradle会选择目前版本作为最终版本
force = true
}
}
动态版本和可变模块
在我们开发的过程中,我们可能想要让某个依赖能够一直保持一个为最终版本,或者我们开发的library需要一个特定版本范围的依赖,而可变模块是Gradle使用我们给个那个版本,但是module的内容是可以改变的,可变版本的缓存时间默认为12小时,但是也是可以配置的
apply plugin: 'java' //so that there are some configurations
configurations.all {
resolutionStrategy {
// 动态版本的缓存时间为10分钟
cacheDynamicVersionsFor 10*60, 'seconds'
// 可变模块的缓存时间为0,也就是不缓存
cacheChangingModulesFor 0, 'seconds'
}
}
Plugin 插件
事实上Gradle内核基本没有提供出真正意义上的构建功能,Gradle所提供的丰富的功能都是通过插件的形式来提供的,从形式上Gradle的插件分为两种
- Script Plugin 本质是就是一个build脚本,和我们的build.gradle是一样的,只是进行了更深入的配置
- Binary Plugin 实现类Plugin接口的类,真正意义上的扩展了Gradle,添加了新的功能和DSL
Plugin在使用时也分两步,解析和应用,解析过程是找到正确版本的pulgin,添加它的脚本classpath,script plugin在应用时,会自解析,而对于一些常用binaryPlugin,Gradle也会自动提供解析,应用的过程就是执行Plugin.apply(T)
,这样我们就可以直接在project中使用插件中的配置,然而由于使用过程中,解析和应用都是一体的,我们并不会感觉到这个过程
Script Plugin
script plugin很简单,它会自解析,同时支持本地文件和远程地址,如果本地project文件系统内的,可以直接在build.gradle中添加
apply from: 'other.gradle'
就直接应用到我们的脚本中,如果是远程地址,则需要一个http Url
Binary Plugin
要应用一个Binary Plugin,我们需要知道plugin id,对于一些核心的插件,Gradle提供了short name ,比如java插件直接使用’java’就可以了,而对于其他的我们需要知道全名
Apply Plugin
要应用一个Binary插件,我们常用的方式就是
apply plugin: 'java'
但是对于很多非Gradle核心插件,比如android插件,我们没法直接使用apply plugin
来获得,对于这类插件我们需要先把插件地址添加到buildscript的classpath中
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.2'
}
}
apply plugin: 'com.android.application'
通过上面这个方法,我们就可以把远程仓库中plugin的jar包添加到我们project中,前面在依赖中我们已经提过,classpath就是一种configruations,gradle会在需要的时候获取它
总结
如果用脚本去理解gradle,gradle就是脚本,但是如果用类java的思想去看待gradle,gradle其实也是一种编码,本文也只是算是一个gradle的简单介绍,gradle博大精深,很多精髓我还没有领会到,见谅了 !