声明:本文转载于简书,原文作者:渡过.
前言:
之前写了一篇关于Gradle渠道定制,虽然说按照官方API可以实现我们的相关的定制化需求,但是对于里面的一些Gradle一些知识并没有特别了解,比如为什么Gradle可以帮我们完成我们定制化的需求,我们在Gradle中Android{ }括号内为什么可以填写buildTypes、productFlavor等等,这些都是让我感到很困惑的地方,经过前一段时间对Gradle的学习,在此做个学习总结,同时也希望能帮助到大家。
目录
1.什么是Gradle
2.Grovvy初探
(1)基本数据类型和容器
(2)基本语法
(3)闭包
(4)Grovvy的建构者模式(Grovvy实现建构的方式)
3.Gradle建构
(1)Gradle工作流程
(2)Gradle三种重要的对象
(3)Gradle Task
(4)三个例子
什么是Gradle?
一个建构工具,同时它也是一个编程框架。
什么是建构?
简单来说就是输入信息,执行命令,得到产物。在项目中建构就是告诉我们的项目,哪些是源码,哪些是资源,编译文件,如何打包......完成这一系列工作,得到最后的产物。Gradle是一个新型的建构的工具,它所使用Grovvy比传统的XML更具优势,可以实现if{某条件成立,编译某文件}/else{编译其他文件}这样有不同条件的任务。Gradle另外一个特点就是它是一种DSL,即Domain Specific Language,领域相关语言。什么是DSL,说白了它是某个行业中的行话。所以我们需要明白的一点是,我们编写脚本是离不开API文档的。
构建工具和编程框架
对我们是用来说,熟悉API,通过API写建构脚本,那么就是一个工具,比如说我们的使用Android gradle plugin对我们来说就是工具。
我们项目中比如使用了微信的AndResGuard(资源混淆压缩),是使用gradle编写的插件,对于他们来说Gradle就是编程框架。
在这里,对我们项目需求来看,我们只需将其作为工具看待,了解API,完成我们的项目构建即可。但是只知其燃不知其所以燃显然不是我们程序员的风格,了解一些必要的原理,可以帮助我们更好地理解Gradle。
Gradle 是使用Grovvy语言来进行构建的,理解gradle就有必要了解Grovvy语言。
Grovvy初探
API文档:http://docs.groovy-lang.org/latest/html/groovy-jdk/
Grovvy语言是一种JVM的动态语言。Groovy语言内部会将其编译成Java class然后启动虚拟机来执行。作为动态语言,Grovvy世界中的所有事物都是对象。
1.基本数据类型和容器
跟java基本数据类型相同
Grovvy中容器(三种)
List:链表,其底层对应Java中的List接口,一般用ArrayList作为真正的实现类。
Map:键-值表,其底层对应Java中的LinkedHashMap。
Range:范围,它其实是List的一种拓展
2.基本语法
定义变量
def variable1 = 1 // 定义一个变量,可不用指定类型,结尾换行不用分号
def int x = 1 //变量定义时,也可以直接指定类型
def aList = [5,'string',true] // 定义数组
def aMap = ['key1':'value1','key2':true] // 定义Map
def aRange = 1..5 //Range类型的变量 由begin值+两个点+end值表示
左边这个aRange包含1,2,3,4,5这5个值
println aRange.from //1
println aRange.to //5
方法定义
def nonReturnTypeFunc(){
last_line //最后一行代码的执行结果就是本函数的返回值
}
String testFunction(arg1,arg2){ //无需指定参数类型
...
}
注意:根据Groovy的原则,如果一个类中有名为xxyyzz这样的属性(其实就是成员变量),Groovy会自动为它添加getXxyyzz和setXxyyzz两个函数,用于获取和设置xxyyzz属性值。
例如:上面例子aRange.from 和aRange.to
![](https://i-blog.csdnimg.cn/blog_migrate/be7d6e8904816b291501bee63928580b.webp?x-image-process=image/format,png)
其实Range类中存在这个两个参数
![](https://i-blog.csdnimg.cn/blog_migrate/5b7748d91e5720bc52d61c1b70551a94.webp?x-image-process=image/format,png)
但是有没有发现,这两个变量是private的,而aRang.to和aRange.from其实是执行了getTo和getFrom的方法!
3.闭包
闭包,是一种数据类型,它代表了一段可执行的代码。从C/C++语言的角度看,闭包和函数指针很像
闭包的定义:
def aClosure = { //闭包是一段代码,所以需要用花括号括起来..
String param1, int param2 -> //箭头很关键。箭头前面是参数定义,箭头后面是代码
println "this is code" //这是代码,最后一句是返回值,
//也可以使用return,和Groovy中普通函数一样
}
使用:
aClosure.call("this is string",100) 或者
aClosure("this is string", 100)
更多时候,闭包将以一个方法的参数使用。
类似我们的回调。
回调回调!!
![](https://i-blog.csdnimg.cn/blog_migrate/3761f1c92d0acad48731dae197d43f7a.webp?x-image-process=image/format,png)
当这个方法有个闭包的参数作为最后一个参数时有个特点:省略圆括号
比如定义一个数组:
def aList = [1,2,3,4,5]
一般我们可能这样去执行each方法:
aList.each({
item->println(item)
})
更多的是使用省略圆括号的方式:
aList.each{ //这行这个数组的each函数,这个each函数传入一个闭包
item->println item //item是闭包的参数
}
关于上面的这个例子,这里需要理解两个点:
1.each(Closure closure)这个方法需要传入一个为闭包数据类型的参数
2.这个closure参数它也会接受一个参数,这个参数是调用者调用这个each方法的时候传入的,上面的item就是这个closure接收的参数,然后把item打印出来
问题:这个传入闭包的item参数是哪里来的呢?
我们先查看API文档,查看方法名
![](https://i-blog.csdnimg.cn/blog_migrate/80d48fe512b88c5a85edc22645df665d.webp?x-image-process=image/format,png)
方法解释,遍历这个List,把每一个元素传递给这个闭包!
所以上面的方法是遍历的了这个aList,打印了每一个元素。
所以Closure的使用依赖于你对API的熟悉程度,但是还有一个约定俗成方式去判断传入闭包的参数,就是看方法名,一般我们可以通过方法名去判断传入闭包的参数,例如上面的each就是把每一个元素传递闭包。
4.Grovvy的建构者模式(Grovvy实现建构的方式)
节选自:
http://wiki.jikexueyuan.com/project/groovy-introduction/domain-specific-languages.html
这里主要介绍为什么在闭包里面的填写的参数可以设置成自己定义的参数,有兴趣可以去查看一下上面的链接,这里节选了建构者这一章节。
Groovy 是构建 DSL 的一种选择平台。使用闭包可以非常轻松地创建自定义控制结构,创建构建者也非常方便,我们很常可以看到这样的定义:
email {
from 'dsl-guru@mycompany.com'
to 'john.doe@waitaminute.com'
subject 'The pope has resigned!'
body {
p 'Really, the pope has resigned!'
}
}
使用构建者策略可实现,利用一个参数为闭包的名为 email 的方法,它会将随后的调用委托给一个对象,该对象实现了 from、to、subject 及 body 各方法。body 方法使用闭包做参数,使用的是构建者策略。
实现这样的构建者往往要通过下面的方式:
def email(Closure cl) {
def email = new EmailSpec()
def code = cl.rehydrate(email, this, this)
code.resolveStrategy = Closure.DELEGATE_ONLY
code()
}
EmailSpec 类实现了 from、to 等方法,通过调用 rehydrate,创建了一个闭包副本,用于为该副本设置 delegate、owner 及 thisObject 等值。设置 owner 和 thisObject 并不十分重要,因为将使用 DELEGATE_ONLY 策略,解决方法调用只针对的是闭包委托。
class EmailSpec {
void from(String from) { println "From: $from"}
void to(String... to) { println "To: $to"}
void subject(String subject) { println "Subject: $subject"}
void body(Closure body) {
def bodySpec = new BodySpec()
def code = body.rehydrate(bodySpec, this, this)
code.resolveStrategy = Closure.DELEGATE_ONLY
code()
}
}
The EmailSpec 类自身的 body 方法将接受一个复制并执行的闭包,这就是 Groovy 构建者模式的原理。
代码中的一个问题在于,email 方法的用户并不知道他能在闭包内调用的方法。唯一的了解途径大概就是方法文档。
还有一个方式是,编译时解释委托策略@DelegatesTo。
def email(@DelegatesTo(EmailSpec) Closure cl) {
def email = new EmailSpec()
def code = cl.rehydrate(email, this, this)
code.resolveStrategy = Closure.DELEGATE_ONLY
code()
}
大致的作用就是通过这个@DelegatesTo 让编译器知道这和Closure代理给谁,可以更智能显示填写哪些参数,方便编程,在这里就不扩展了。
通过上面我们了解了,Grovvy的基本语法和闭包的基本使用,了解Grovvy建构的基本概念是通过代理来完成,那么在Gradle是怎么使用Grovvy来完成我们的构建呢?
Gradle建构
Gradle是一个框架,它定义一套自己的游戏规则,必须要遵守它设计的规则。
Gradle中,每一个待编译的工程都叫一个Project。每一个Project在构建的时候都包含一系列的Task。比如一个Android APK的编译可能包含:Java源码编译Task、资源编译Task、JNI编译Task、lint检查Task、打包生成APK的Task、签名Task等。一个具体的编译过程是由一个一个的Task来定义和执行的。
我们首先通过项目工程目录,来理解Gradle设计的规则
![](https://i-blog.csdnimg.cn/blog_migrate/06ecf43d4e2d902fe36be058411ee265.webp?x-image-process=image/format,png)
settings.gradle表示导入的哪些项目,具体来说就是gradle需要编译哪些项目。
![](https://i-blog.csdnimg.cn/blog_migrate/f397bbebed8f1668c2ad1775862cf23d.webp?x-image-process=image/format,png)
如图,导入了app这个工程。
在主目录MyApplication工程目录下有个build.gradle
![](https://i-blog.csdnimg.cn/blog_migrate/4b29112084ebda855b345c88c8ad16b2.webp?x-image-process=image/format,png)
我们看到最上面的注释,这个文件是level最高级的配置文件,这里配置Android gradle plugin版本,这里我的是2.2.0的版本,allprojects配置所有Project的需要的配置。
![](https://i-blog.csdnimg.cn/blog_migrate/9861066c1b9c5ec6eda83d7f8ac7f9e1.webp?x-image-process=image/format,png)
为了解决Gradle版本不断升级,不同时期的项目编译不同版本的Gradle,Google推出了gradle wrapper,这个就放置在gradle文件夹中。
注意:这里dependencies配置的是Android-gradle-plugin版本而不是Gradle的版本。
扩展: 导入开源项目的正确姿势
在我们导入GitHub项目的时候经常会遇到一直在下载Gradle,打不开项目的问题。原因就是要导入的项目Android-gradle-plugin和Gradle版本本地不存在,所以导入项目是会先去下载,但是下载又因为墙的原因就会导致在载入界面卡主。导入项目正确的姿势是:
1.打开本地可以编译的项目,
2.将这个项目中的依赖的Android-gradle-plugin版本和gradle目录拷贝替换到需要导入的项目中就可以打开这个项目了。
继续查看项目目录
![](https://i-blog.csdnimg.cn/blog_migrate/27418a42ae55a3aa79ceaef4ea052a60.webp?x-image-process=image/format,png)
在app目录中也存在一个build.gradle文件,内容后面再讲解。其实每一个setting.gradle 中include进来的Project都需要配置build.gradle文件。
之前我们提到导入的每一个项目都是一个Project,每个Project又包含了许多Task。查看Task,可以在终端执行gradlew tasks,或者通过studio提供可视化工具操作。
![](https://i-blog.csdnimg.cn/blog_migrate/b2f097d70f1b57a8e17a5a8b983680d7.webp?x-image-process=image/format,png)
之前说到的这些Task包含Java源码编译Task、资源编译Task、JNI编译Task、lint检查Task、打包生成APK的Task、签名Task等等。
了解了Gradle的每个Project包含了Task,那么编译的过程是怎么把Task联系起来呢?
1.Gradle工作流程
![](https://i-blog.csdnimg.cn/blog_migrate/e78875e683b0ce075e4fc3f921af285b.webp?x-image-process=image/format,png)
1.(Initiliazation phase)首先是初始化阶段。就是执行settings.gradle Initiliazation phase的下一个阶段是Configration阶段。
2.(Configration phase)Configration阶段的目标是解析每个project中的build.gradle。比如上面例子中,解析每个子目录中的build.gradle。在这两个阶段之间,我们可以加一些定制化的Hook(钩子)。这当然是通过API来添加的。
3.(Execution phase)Configuration阶段完了后,整个build的project以及内部的Task关系就确定了。前面说过,一个Project包含很多Task,每个Task之间有依赖关系。Configuration会建立一个有向图来描述Task之间的依赖关系。所以,我们可以添加一个HOOK,即当Task关系图建立好后,执行一些操作。
关于Gradle的工作流程,我们需要记住:
Gradle有一个初始化流程,这个时候settings.gradle会执行。
在配置阶段,每个Project都会被解析,其内部的任务也会被添加到一个有向图里,用于解决执行过程中的依赖关系。
然后才是执行阶段。你在gradlew xxx中指定什么任务,gradle就会将这个xxx任务链上的所有任务全部按依赖顺序执行一遍!
2.Gradle三种重要的对象
Gradle对象:当我们执行gradlew xxx或者什么的时候,gradle会从默认的配置脚本中构造出一个Gradle对象。在整个执行过程中,只有这么一个对象。Gradle对象的数据类型就是Gradle。我们一般很少去定制这个默认的配置脚本。
Project对象:每一个build.gradle会转换成一个Project对象。加载插件,设置不同的配置,比如Application plugin插件和Library plugin插件gradle配置是不太一样的。
Settings对象:显然,每一个settings.gradle都会转换成一个Settings对象。
(https://docs.gradle.org/current/dsl/ )文档查看
Project对象:
文档:https://docs.gradle.org/current/dsl/org.gradle.api.Project.html
我们知道每一个项目在build.gradle脚本解析成一个Project对象,在build.gradle我们可以查看这个Project的变量,Project包含了许多Task,tasks就是任务的集合
![](https://i-blog.csdnimg.cn/blog_migrate/1bdb280b66c875fa3d2e6a5ce3b431c9.webp?x-image-process=image/format,png)
3.Gradle Task
Gradle Task是Gradle非常重要的概念,Task是Gradle中的一种数据类型。它代表了一些要执行或者要干的工作。不同的插件可以添加不同的Task。每一个Task都需要和一个Project关联。
(Task的API文档位于https://docs.gradle.org/current/dsl/org.gradle.api.Task.html)
Task 定义(这个是在Project中定义的,Project文档!)
task helloworld { //调用的是Project中的方法task(name, configureClosure)
println('hello world!')
doFirst{
println('do first')
}
do Last{
println('do last')
}
}
上面代码的意思就是定义了一个Task,Task的名字为helloworld
doFirst和doLast都给这个Task添加了Action。
注意:上面println('hello world')会在配置阶段就会执行,doFirst和doLast不会执行,通过执行gradlew helloworld才会顺序执行doFirst和doLast。
<<符号代表 do last
task helloworld << {
println('do last')
}
Task 依赖
testTask0.dependsOn testTask1 //执行testTask0将会先执行testTask1
4.三个例子:
![](https://i-blog.csdnimg.cn/blog_migrate/a599ebb84f97afac70e2e6d0f63d1797.webp?x-image-process=image/format,png)
![](https://i-blog.csdnimg.cn/blog_migrate/a1c3e9006b7b771683ac0d4cccca60ea.webp?x-image-process=image/format,png)
![](https://i-blog.csdnimg.cn/blog_migrate/c406f080f163d979934e384bc786f3c1.webp?x-image-process=image/format,png)
这三个都是直接是写在app工程下gradle中,project.afterEvalute意思是创建了任务有向图之后。写一下上面的代码,编译并运行gradlew assmbleDebug,查看输出。深入理解上面例子的执行过程。
解析
第一个,编译报错。
![](https://i-blog.csdnimg.cn/blog_migrate/798ad36315eeffb6226dd77a96932e12.webp?x-image-process=image/format,png)
上面说这个任务在app这个project中找不到,我们之前说过gradle是有生命周期的,我们在配置阶段完成之后才会添加project中形成有向图,这段代码是在配置之前就执行了,有向图没有形成,自然找不到这个任务,从而报错。
第二个,编译时输出。
![](https://i-blog.csdnimg.cn/blog_migrate/d13c33ed8cd0591efdef559f333f7ebf.webp?x-image-process=image/format,png)
我们看到在编译过程就输出了这个代码,而且确实是在配置(Configuration)生命周期之后,因为我们并没有给这个任务添加Action,所以println会在getByName方法中一并输出。
第三个,编译不输出,执行gradlew assmbleDebug输出
![](https://i-blog.csdnimg.cn/blog_migrate/7dafc4f5535f7d976b7934eb6ccd292b.webp?x-image-process=image/format,png)
这是因为我们在这个Task添加doLast的Action,说明println的动作会在这个Task的最后执行,也就是我们执行这个assembleDebug任务的时候最后执行。
部分内容转载:
http://www.infoq.com/cn/articles/android-in-depth-gradle/
http://wiki.jikexueyuan.com/project/groovy-introduction/domain-specific-languages.html
推荐阅读:
http://www.infoq.com/cn/articles/android-in-depth-gradle/ (深入理解Android:Gradle详解)墙裂推荐!
http://google.github.io/android-gradle-dsl/current/index.html (Android Plugin DSL Reference)
http://docs.groovy-lang.org/latest/html/groovy-jdk/ (Groovy API)