构建,叫build也好,叫make也行。反正就是根据输入信息然后干一堆事情,最后得到几个产出物(Artifact)。最最简单的构建工具就是make了。make就是根据Makefile文件中写的规则,执行对应的命令,然后得到目标产物。日常生活中,和构建最类似的一个场景就是做菜。输入各种食材,然后按固定的工序,最后得到一盘菜。当然,做同样一道菜,由于需求不同,做出来的东西也不尽相同。比如,宫保鸡丁这道菜,回民要求不能放大油、口淡的要求少放盐和各种油、辣不怕的男女汉子们可以要求多放辣子....总之,做菜包含固定的工序,但是对于不同条件或需求,需要做不同的处理。
在Gradle爆红之前,常用的构建工具是ANT,然后又进化到Maven。ANT和Maven这两个工具其实也还算方便,现在还有很多地方在使用。但是二者都有一些缺点,所以让更懒得人觉得不是那么方便。比如,Maven编译规则是用XML来编写的。XML虽然通俗易懂,但是很难在xml中描述if{某条件成立,编译某文件}/else{编译其他文件}这样有不同条件的任务。
怎么解决?怎么解决好?对程序员而言,自然是编程解决,但是有几个小要求:
Groovy是一种动态语言。这种语言比较有特点,它和Java一样,也运行于Java虚拟机中。简单粗暴点儿看,你可以认为Groovy扩展了Java语言。比如,Groovy对自己的定义就是:Groovy是在java平台上的、具有像Python,Ruby和Smalltalk语言特性的灵活动态语言,Groovy保证了这些特性像Java语法一样被Java开发者使用。除了语言和Java相通外,Groovy有时候又像一种脚本语言。前文也提到过,当我执行Groovy脚本时,Groovy会先将其编译成Java类字节码,然后通过Jvm来执行这个Java类。图1展示了Java、Groovy和Jvm之间的关系。
实际上,由于GroovyCode在真正执行的时候已经变成了Java字节码,所以JVM根本不知道自己运行的是Groovy代码。下面我们将介绍Groovy。由于此文的主要目的是Gradle,所以我们不会过多讨论Groovy中细枝末节的东西,而是把知识点集中在以后和Gradle打交道时一些常用的地方上。
然后,创建一个test.groovy文件,里边只有一行代码:
亲们,必须要完成上面的操作啊。做完后,有什么感觉和体会?
最大的感觉可能就是groovy和shell脚本,或者python好类似。
为了后面讲述方面,这里先介绍一些前提知识。初期接触可能有些别扭,看习惯就好了。
defvariable1=1//可以不使用分号结尾defvarable2="Iamaperson"defintx=1//变量定义时,也可以直接指定类型StringtestFunction(arg1,arg2){//无需指定参数类型...}//无类型的函数定义,必须使用def关键字
defnonReturnTypeFunc(){last_line//最后一行代码的执行结果就是本函数的返回值}//如果指定了函数返回类型,则可不必加def关键字来定义函数StringgetString(){return"Iamastring"}其实,所谓的无返回类型的函数,我估计内部都是按返回Object类型来处理的。毕竟,Groovy是基于Java的,而且最终会转成JavaCode运行在JVM上
//下面这个函数的返回值是字符串"getSomethingreturnvalue"defgetSomething(){"getSomethingreturnvalue"//如果这是最后一行代码,则返回类型为String1000//如果这是最后一行代码,则返回类型为Integer}注意,如果函数定义时候指明了返回值类型的话,函数中则必须返回正确的数据类型,否则运行时报错。如果使用了动态类型的话,你就可以返回任何类型了。
1单引号''中的内容严格对应Java中的String,不对$符号进行转义
defsingleQuote='Iam$dolloar'//输出就是Iam$dolloar2双引号""的内容则和脚本语言的处理有点像,如果字符中有$号的话,则它会$表达式先求值。
defdoubleQuoteWithoutDollar="Iamonedollar"//输出Iamonedollardefx=1defdoubleQuoteWithDollar="Iam$xdolloar"//输出Iam1dolloar3三个引号'''xxx'''中的字符串支持随意换行比如
defmultieLines='''beginline1line2end'''println("test")--->println"test"注意,虽然写代码的时候,对于函数调用可以不带括号,但是Groovy经常把属性和函数调用混淆。比如
defgetSomething(){"hello"}getSomething()//如果不加括号的话,Groovy会误认为getSomething是一个变量。
所以,调用函数要不要带括号,我个人意见是如果这个函数是GroovyAPI或者GradleAPI中比较常用的,比如println,就可以不带括号。否则还是带括号。Groovy自己也没有太好的办法解决这个问题,只能兵来将挡水来土掩了。
好了,了解上面一些基础知识后,我们再介绍点深入的内容。
Groovy中的数据类型我们就介绍两种和Java不太一样的:
放心,这里介绍的东西都很简单
3.3.1基本数据类型
作为动态语言,Groovy世界中的所有事物都是对象。所以,int,boolean这些Java中的基本数据类型,在Groovy代码中其实对应的是它们的包装数据类型。比如int对应为Integer,boolean对应为Boolean。比如下图中的代码执行结果:
图4int实际上是Integer
3.3.2容器类
Groovy中的容器类很简单,就三种:
对容器而言,我们最重要的是了解它们的用法。下面是一些简单的例子:
1.List类
变量定义:List变量由[]定义,比如defaList=[5,'string',true]//List由[]定义,其元素可以是任何对象变量存取:可以直接通过索引存取,而且不用担心索引越界。如果索引超过当前链表长度,List会自动往该索引添加元素assertaList[1]=='string'assertaList[5]==null//第6个元素为空aList[100]=100//设置第101个元素的值为10assertaList[100]==100//那么,aList到现在为止有多少个元素呢?printlnaList.size===>结果是1012.Map类
容器变量定义变量定义:Map变量由[:]定义,比如defaMap=['key1':'value1','key2':true]Map由[:]定义,注意其中的冒号。冒号左边是key,右边是Value。key必须是字符串,value可以是任何对象。另外,key可以用''或""包起来,也可以不用引号包起来。比如defaNewMap=[key1:"value",key2:true]//其中的key1和key2默认被处理成字符串"key1"和"key2"//不过Key要是不使用引号包起来的话,也会带来一定混淆,比如defkey1="wowo"defaConfusedMap=[key1:"whoami"]//aConfuseMap中的key1到底是"key1"还是变量key1的值“wowo”?显然,答案是字符串"key1"。如果要是"wowo"的话,则aConfusedMap的定义必须设置成:defaConfusedMap=[(key1):"whoami"]//Map中元素的存取更加方便,它支持多种方法:printlnaMap.keyName//<==这种表达方法好像key就是aMap的一个成员变量一样printlnaMap['keyName']//<==这种表达方法更传统一点aMap.anotherkey="iammap"//<==为map添加新元素3.Range类Range是Groovy对List的一种拓展,变量定义和大体的使用方法如下:
defaRange=1..5//<==Range类型的变量由begin值+两个点+end值表示//左边这个aRange包含1,2,3,4,5这5个值//如果不想包含最后一个元素,则defaRangeWithoutEnd=1..<5<==包含1,2,3,4这4个元素printlnaRange.fromprintlnaRange.to3.3.4GroovyAPI的一些秘笈
前面讲这些东西,主要是让大家了解Groovy的语法。实际上在coding的时候,是离不开SDK的。由于Groovy是动态语言,所以要使用它的SDK也需要掌握一些小诀窍。
以上文介绍的Range为例,我们该如何更好得使用它呢?
有了API文档,你就可以放心调用其中的函数了。不过,不过,不过:我们刚才代码中用到了Range.from/to属性值,但翻看RangeAPI文档的时候,其实并没有这两个成员变量。图6是Range的方法
文档中并没有说明Range有from和to这两个属性,但是却有getFrom和getTo这两个函数。Whathappened?原来:
根据Groovy的原则,如果一个类中有名为xxyyzz这样的属性(其实就是成员变量),Groovy会自动为它添加getXxyyzz和setXxyyzz两个函数,用于获取和设置xxyyzz属性值。
注意,get和set后第一个字母是大写的。所以,当你看到Range中有getFrom和getTo这两个函数时候,就得知道潜规则下,Range有from和to这两个属性。当然,由于它们不可以被外界设置,所以没有公开setFrom和setTo函数。
3.4.1闭包的样子
闭包,是一种数据类型,它代表了一段可执行的代码。其外形如下:
defaClosure={//闭包是一段代码,所以需要用花括号括起来..Stringparam1,intparam2->//这个箭头很关键。箭头前面是参数定义,箭头后面是代码println"thisiscode"//这是代码,最后一句是返回值,//也可以使用return,和Groovy中普通函数一样}简而言之,Closure的定义格式是:
defxxx={paramters->code}//或者defxxx={无参数,纯code}这种case不需要->符号说实话,从C/C++语言的角度看,闭包和函数指针很像。闭包定义好后,要调用它的方法就是:
闭包对象.call(参数)或者更像函数指针调用的方法:
闭包对象(参数)
比如:
aClosure.call("thisisstring",100)或者aClosure("thisisstring",100)上面就是一个闭包的定义和使用。在闭包中,还需要注意一点:
如果闭包没定义参数的话,则隐含有一个参数,这个参数名字叫it,和this的作用类似。it代表闭包的参数。
defgreeting={"Hello,$it!"}assertgreeting('Patrick')=='Hello,Patrick!'等同于:
defgreeting={it->"Hello,$it!"}assertgreeting('Patrick')=='Hello,Patrick!'但是,如果在闭包定义时,采用下面这种写法,则表示闭包没有参数!
defnoParamClosure={->true}这个时候,我们就不能给noParamClosure传参数了!
noParamClosure("test")<==报错喔!3.4.2Closure使用中的注意点
1.省略圆括号
闭包在Groovy中大量使用,比如很多类都定义了一些函数,这些函数最后一个参数都是一个闭包。比如:
publicstatic
defiamList=[1,2,3,4,5]//定义一个ListiamList.each{//调用它的each,这段代码的格式看不懂了吧?each是个函数,圆括号去哪了?printlnit}上面代码有两个知识点:
deftestClosure(inta1,Stringb1,Closureclosure){//dosomethingclosure()//调用闭包}//那么调用的时候,就可以免括号!testClosure(4,"test",{println"iaminclosure"})//红色的括号可以不写..注意,这个特点非常关键,因为以后在Gradle中经常会出现图7这样的代码:
经常碰见图7这样的没有圆括号的代码。省略圆括号虽然使得代码简洁,看起来更像脚本语言,但是它这经常会让我confuse(不知道其他人是否有同感),以doLast为例,完整的代码应该按下面这种写法:
doLast({println'Helloworld!'})有了圆括号,你会知道doLast只是把一个Closure对象传了进去。很明显,它不代表这段脚本解析到doLast的时候就会调用println'Helloworld!'。
但是把圆括号去掉后,就感觉好像println'Helloworld!'立即就会被调用一样!
2.如何确定Closure的参数
另外一个比较让人头疼的地方是,Closure的参数该怎么搞?还是刚才的each函数:
publicstatic
defiamList=[1,2,3,4,5]//定义一个List变量iamList.each{//调用它的each函数,只要传入一个Closure就可以了。printlnit}看起来很轻松,其实:
我们能写成下面这样吗?
iamList.each{Stringname,intx->returnx}//运行的时候肯定报错!所以,Closure虽然很方便,但是它一定会和使用它的上下文有极强的关联。要不,作为类似回调这样的东西,我如何知道调用者传递什么参数给Closure呢?
此问题如何破解?只能通过查询API文档才能了解上下文语义。比如下图8:
图8中:
对Map的findAll而言,Closure可以有两个参数。findAll会将Key和Value分别传进去。并且,Closure返回true,表示该元素是自己想要的。返回false表示该元素不是自己要找的。示意代码如图9所示:
Closure的使用有点坑,很大程度上依赖于你对API的熟悉程度,所以最初阶段,SDK查询是少不了的。
最后,我们来看一下Groovy中比较高级的用法。
3.5.1脚本类
1.脚本中import其他类
Groovy中可以像Java那样写package,然后写类。比如在文件夹com/cmbc/groovy/目录中放一个文件,叫Test.groovy,如图10所示:
现在,我们在测试的根目录下建立一个test.groovy文件。其代码如下所示:
你看,test.groovy先import了com.cmbc.groovy.Test类,然后创建了一个Test类型的对象,接着调用它的print函数。
这两个groovy文件的目录结构如图12所示:
在groovy中,系统自带会加载当前目录/子目录下的xxx.groovy文件。所以,当执行groovytest.groovy的时候,test.groovyimport的Test类能被自动搜索并加载到。
2.脚本到底是什么
Java中,我们最熟悉的是类。但是我们在Java的一个源码文件中,不能不写class(interface或者其他....),而Groovy可以像写脚本一样,把要做的事情都写在xxx.groovy中,而且可以通过groovyxxx.groovy直接执行这个脚本。这到底是怎么搞的?
既然是基于Java的,Groovy会先把xxx.groovy中的内容转换成一个Java类。比如:
test.groovy的代码是:
println'Groovyworld!'Groovy把它转换成这样的Java类:
执行groovyc-dclassestest.groovy
groovyc是groovy的编译命令,-dclasses用于将编译得到的class文件拷贝到classes文件夹下
图13是test.groovy脚本转换得到的javaclass。用jd-gui反编译它的代码:
图13中:
groovyc是一个比较好的命令,读者要掌握它的用法。然后利用jd-gui来查看对应class的Java源码。
3.脚本中的变量和作用域
前面说了,xxx.groovy只要不是和Java那样的class,那么它就是一个脚本。而且脚本的代码其实都会被放到run函数中去执行。那么,在Groovy的脚本中,很重要的一点就是脚本中定义的变量和它的作用域。举例:
defx=1<==注意,这个x有def(或者指明类型,比如intx=1)defprintx(){printlnx}printx()<==报错,说x找不到
为什么?继续来看反编译后的class文件。
图14中:
printx被定义成test类的成员函数
defx=1,这句话是在run中创建的。所以,x=1从代码上看好像是在整个脚本中定义的,但实际上printx访问不了它。printx是test成员函数,除非x也被定义成test的成员函数,否则printx不能访问它。
那么,如何使得printx能访问x呢?很简单,定义的时候不要加类型和def。即:
x=1<==注意,去掉def或者类型defprintx(){printlnx}printx()<==OK这次Java源码又变成什么样了呢?
图15中,x也没有被定义成test的成员函数,而是在run的执行过程中,将x作为一个属性添加到test实例对象中了。然后在printx中,先获取这个属性。
注意,Groovy的文档说x=1这种定义将使得x变成test的成员变量,但从反编译情况看,这是不对的.....
虽然printx可以访问x变量了,但是假如有其他脚本却无法访问x变量。因为它不是test的成员变量。
比如,我在测试目录下创建一个新的名为test1.groovy。这个test1将访问test.groovy中定义的printx函数:
这种方法使得我们可以将代码分成模块来编写,比如将公共的功能放到test.groovy中,然后使用公共功能的代码放到test1.groovy中。
执行groovytest1.groovy,报错。说x找不到。这是因为x是在test的run函数动态加进去的。怎么办?
importgroovy.transform.Field;//必须要先import@Fieldx=1<==在x前面加上@Field标注,这样,x就彻彻底底是test的成员变量了。查看编译后的test.class文件,得到:
这个时候,test.groovy中的x就成了test类的成员函数了。如此,我们可以在script中定义那些需要输出给外部脚本或类使用的变量了!
3.5.2文件I/O操作
本节介绍下Groovy的文件I/O操作。直接来看例子吧,虽然比Java看起来简单,但要理解起来其实比较难。尤其是当你要自己查SDK并编写代码的时候。
整体说来,Groovy的I/O操作是在原有JavaI/O操作上进行了更为简单方便的封装,并且使用Closure来简化代码编写。主要封装了如下一些了类:
1.读文件
Groovy中,文件读操作简单到令人发指:
deftargetFile=newFile(文件名)<==File对象还是要创建的。
看看Groovy定义的API:
1读该文件中的每一行:eachLine的唯一参数是一个Closure。Closure的参数是文件每一行的内容
其内部实现肯定是Groovy打开这个文件,然后读取文件的一行,然后调用Closure...
targetFile.eachLine{StringoneLine->printlnoneLine<==是不是令人发指??!2直接得到文件内容
targetFile.getBytes()<==文件内容一次性读出,返回类型为byte[]注意前面提到的getter和setter函数,这里可以直接使用targetFile.bytes//....
defism=targetFile.newInputStream()//操作ism,最后记得关掉ism.close4使用闭包操作inputStream,以后在Gradle里会常看到这种搞法
2.写文件
和读文件差不多。不再啰嗦。这里给个例子,告诉大家如何copy文件。
defsrcFile=newFile(源文件名)deftargetFile=newFile(目标文件名)targetFile.withOutputStream{os->srcFile.withInputStream{ins->os< 再一次向极致简单致敬。但是,SDK恐怕是离不开手了... 3.5.3XML操作 GPath功能包括:给个例子好了,来自Groovy官方文档。 test.xml文件: Anyway,从使用角度看,尤其是又限定在Gradle这个领域内,能用到的都是Groovy中一些简单的知识。 现在正式进入Gradle。Gradle是一个工具,同时它也是一个编程框架。前面也提到过,使用这个工具可以完成app的编译打包等工作。当然你也可以用它干其他的事情。 Gradle是什么?学习它到什么地步就可以了? =====>看待问题的时候,所站的角度非常重要。 -->当你把Gradle当工具看的时候,我们只想着如何用好它。会写、写好配置脚本就OK -->当你把它当做编程框架看的时候,你可能需要学习很多更深入的内容。 另外,今天我们把它当工具看,明天因为需求发生变化,我们可能又得把它当编程框架看。 最后,设置~/.bashrc,把Gradle加到PATH里,如图20所示: 执行source~/.bashrc,初始化环境。 执行gradle--version,如果成功运行就OK了。 注意,为什么说Gradle是一个编程框架?来看它提供的API文档: 原来,我们编写所谓的编译脚本,其实就是玩Gradle的API....所以它从更底层意义上看,是一个编程框架! 既然是编程框架,我在讲解Gradle的时候,尽量会从API的角度来介绍。有些读者肯定会不耐烦,为嘛这么费事? 而从API角度来看待Gradle的话,有了SDK文档,你就可以编程。编程是靠记住一行行代码来实现的吗?不是,是在你掌握大体流程,然后根据SDK+API来完成的! 其实,Gradle自己的UserGuide也明确说了: Buildscriptsarecode Gradle是一个框架,它定义一套自己的游戏规则。我们要玩转Gradle,必须要遵守它设计的规则。下面我们来讲讲Gradle的基本组件: Gradle中,每一个待编译的工程都叫一个Project。每一个Project在构建的时候都包含一系列的Task。比如一个AndroidAPK的编译可能包含:Java源码编译Task、资源编译Task、JNI编译Task、lint检查Task、打包生成APK的Task、签名Task等。 一个Project到底包含多少个Task,其实是由编译脚本指定的插件决定。插件是什么呢?插件就是用来定义Task,并具体执行这些Task的东西。 刚才说了,Gradle是一个框架,作为框架,它负责定义流程和规则。而具体的编译工作则是通过插件的方式来完成的。比如编译Java有Java插件,编译Groovy有Groovy插件,编译AndroidAPP有AndroidAPP插件,编译AndroidLibrary有AndroidLibrary插件 好了。到现在为止,你知道Gradle中每一个待编译的工程都是一个Project,一个具体的编译过程是由一个一个的Task来定义和执行的。 4.2.1一个重要的例子 下面我们来看一个实际的例子。这个例子非常有代表意义。图22是一个名为posdevice的目录。这个目录里包含3个AndroidLibrary工程,2个AndroidAPP工程。 在图22的例子中: 请回答问题,在上面这个例子中,有多少个Project? 答案是:每一个Library和每一个App都是单独的Project。根据Gradle的要求,每一个Project在其根目录下都需要有一个build.gradle。build.gradle文件就是该Project的编译脚本,类似于Makefile。 看起来好像很简单,但是请注意:posdevice虽然包含5个独立的Project,但是要独立编译他们的话,得: 这很麻烦啊,有10个独立Project,就得重复执行10次这样的命令。更有甚者,所谓的独立Project其实有依赖关系的。比如我们这个例子。 那么,我想在posdevice目录下,直接执行gradleassemble,是否能把这5个Project的东西都编译出来呢? 答案自然是可以。在Gradle中,这叫Multi-ProjectsBuild。把posdevice改造成支持Gradle的Multi-ProjectsBuild很容易,需要: 来看settings.gradle的内容,最关键的内容就是告诉Gradle这个multiprojects包含哪些子projects: [settings.gradle] //通过include函数,将子Project的名字(其文件夹名)包含进来include'CPosSystemSdk','CPosDeviceSdk','CPosSdkDemo','CPosDeviceServerApk','CPosSystemSdkWizarPosImpl'强烈建议: 如果你确实只有一个Project需要编译,我也建议你在目录下添加一个settings.gradle。我们团队内部的所有单个Project都已经改成支持Multiple-ProjectBuild了。改得方法就是添加settings.gradle,然后include对应的project名字。 另外,settings.gradle除了可以include外,还可以设置一些函数。这些函数会在gradle构建整个工程任务的时候执行,所以,可以在settings做一些初始化的工作。比如:我的settings.gradle的内容: //定义一个名为initMinshengGradleEnvironment的函数。该函数内部完成一些初始化操作//比如创建特定的目录,设置特定的参数等definitMinshengGradleEnvironment(){println"initializeMinshengGradleEnvironment....."......//干一些special的私活....println"initializeMinshengGradleEnvironmentcompletes..."}//settings.gradle加载的时候,会执行initMinshengGradleEnvironmentinitMinshengGradleEnvironment()//include也是一个函数:include'CPosSystemSdk','CPosDeviceSdk','CPosSdkDemo','CPosDeviceServerApk','CPosSystemSdkWizarPosImpl'4.2.2gradle命令介绍 1.gradleprojects查看工程信息 到目前为止,我们了解了Gradle什么呢? 执行gradleprojects,得到图23: 你看,multiprojects的情况下,posdevice这个目录对应的build.gradle叫RootProject,它包含5个子Project。 如果你修改settings.gradle,使得include只有一个参数,则gradleprojects的子project也会变少,比如图24: 2.gradletasks查看任务信息 查看了Project信息,这个还比较简单,直接看settings.gradle也知道。那么Project包含哪些Task信息,怎么看呢?图23,24中最后的输出也告诉你了,想看某个Project包含哪些Task信息,只要执行: gradleproject-path:tasks就行。注意,project-path是目录名,后面必须跟冒号。 对于Multi-project,在根目录中,需要指定你想看哪个poject的任务。不过你要是已经cd到某个Project的目录了,则不需指定Project-path。 来看图25: 图25是gradleCPosSystemSdk:tasks的结果。 这些都是后话,我们以后会详细介绍。 3.gradletask-name执行任务 图25中列出了好多任务,这时候就可以通过gradle任务名来执行某个任务。这和makexxx很像。比如: gradletasks会列出每个任务的描述,通过描述,我们大概能知道这些任务是干什么的.....。然后gradletask-name执行它就好。 这里要强调一点:Task和Task之间往往是有关系的,这就是所谓的依赖关系。比如,assembletask就依赖其他task先执行,assemble才能完成最终的输出。 依赖关系对我们使用gradle有什么意义呢? 如果知道Task之间的依赖关系,那么开发者就可以添加一些定制化的Task。比如我为assemble添加一个SpecialTest任务,并指定assemble依赖于SpecialTest。当assemble执行的时候,就会先处理完它依赖的task。自然,SpecialTest就会得到执行了... 大家先了解这么多,等后面介绍如何写gradle脚本的时候,这就是调用几个函数的事情,NothingSpecial! Gradle的工作流程其实蛮简单,用一个图26来表达: 图26告诉我们,Gradle工作包含三个阶段: 我在: 好了,Hook的代码怎么写,估计你很好奇,而且肯定会埋汰,怎么就还没告诉我怎么写Gradle。马上了! 最后,关于Gradle的工作流程,你只要记住: 下面来告诉你怎么写代码! Gradle基于Groovy,Groovy又基于Java。所以,Gradle执行的时候和Groovy一样,会把脚本转换成Java对象。Gradle主要有三种对象,这三种对象和三种不同的脚本文件对应,在gradle执行的时候,会将脚本转换成对应的对端: 注意,对于其他gradle文件,除非定义了class,否则会转换成一个实现了Script接口的对象。这一点和3.5节中Groovy的脚本类相似 当我们执行gradle的时候,gradle首先是按顺序解析各个gradle文件。这里边就有所所谓的生命周期的问题,即先解析谁,后解析谁。图27是Gradle文档中对生命周期的介绍:结合上一节的内容,相信大家都能看明白了。现在只需要看红框里的内容: 4.4.1Gradle对象 我们先来看Gradle对象,它有哪些属性呢?如图28所示: 我在posdevicebuild.gradle中和settings.gradle中分别加了如下输出: //在settings.gradle中,则输出"Insettings,gradleidis"println"Inposdevice,gradleidis"+gradle.hashCode()println"HomeDir:"+gradle.gradleHomeDirprintln"UserHomeDir:"+gradle.gradleUserHomeDirprintln"Parent:"+gradle.parent得到结果如图29所示: Gradle的函数接口在文档中也有。 4.4.2Project对象 每一个build.gradle文件都会转换成一个Project对象。在Gradle术语中,Project对象对应的是BuildScript。 Project包含若干Tasks。另外,由于Project对应具体的工程,所以需要为Project加载所需要的插件,比如为Java工程加载Java插件。其实,一个Project包含多少Task往往是插件决定的。 所以,在Project中,我们要: 1.加载插件 来看代码: [apply函数的用法] apply是一个函数,此处调用的是图30中最后一个apply函数。注意,Groovy支持函数调用的时候通过参数名1:参数值2,参数名2:参数值2的方式来传递参数 applyplugin:'com.android.library'<==如果是编译Library,则加载此插件applyplugin:'com.android.application'<==如果是编译AndroidAPP,则加载此插件除了加载二进制的插件(上面的插件其实都是下载了对应的jar包,这也是通常意义上我们所理解的插件),还可以加载一个gradle文件。为什么要加载gradle文件呢? 其实这和代码的模块划分有关。一般而言,我会把一些通用的函数放到一个名叫utils.gradle文件里。然后在其他工程的build.gradle来加载这个utils.gradle。这样,通过一些处理,我就可以调用utils.gradle中定义的函数了。 加载utils.gradle插件的代码如下: utils.gradle是我封装的一个gradle脚本,里边定义了一些方便函数,比如读取AndroidManifest.xml中 的versionName,或者是copyjar包/APK包到指定的目录 applyfrom:rootProject.getRootDir().getAbsolutePath()+"/utils.gradle"也是使用apply的最后一个函数。那么,apply最后一个函数到底支持哪些参数呢?还是得看图31中的API说明: 我这里不遗余力的列出API图片,就是希望大家在写脚本的时候,碰到不会的,一定要去查看API文档! 2.设置属性 如果是单个脚本,则不需要考虑属性的跨脚本传播,但是Gradle往往包含不止一个build.gradle文件,比如我设置的utils.gradle,settings.gradle。如何在多个脚本中设置属性呢? Gradle提供了一种名为extraproperty的方法。extraproperty是额外属性的意思,在第一次定义该属性的时候需要通过ext前缀来标示它是一个额外的属性。定义好之后,后面的存取就不需要ext前缀了。ext属性支持Project和Gradle对象。即Project和Gradle对象都可以设置ext属性 举个例子: 我在settings.gradle中想为Gradle对象设置一些外置属性,所以在initMinshengGradleEnvironment函数中 definitMinshengGradleEnvironment(){//属性值从local.properites中读取Propertiesproperties=newProperties()FilepropertyFile=newFile(rootDir.getAbsolutePath()+"/local.properties")properties.load(propertyFile.newDataInputStream())//gradle就是gradle对象。它默认是Settings和Project的成员变量。可直接获取//ext前缀,表明操作的是外置属性。api是一个新的属性名。前面说过,只在//第一次定义或者设置它的时候需要ext前缀gradle.ext.api=properties.getProperty('sdk.api')printlngradle.api//再次存取api的时候,就不需要ext前缀了......}再来一个例子强化一下: 我在utils.gradle中定义了一些函数,然后想在其他build.gradle中调用这些函数。那该怎么做呢? [utils.gradle] //utils.gradle中定义了一个获取AndroidManifests.xmlversionName的函数defgetVersionNameAdvanced(){下面这行代码中的project是谁?defxmlFile=project.file("AndroidManifest.xml")defrootManifest=newXmlSlurper().parse(xmlFile)returnrootManifest['@android:versionName']}//现在,想把这个API输出到各个Project。由于这个utils.gradle会被每一个ProjectApply,所以//我可以把getVersionNameAdvanced定义成一个closure,然后赋值到一个外部属性下面的ext是谁的ext?ext{//此段花括号中代码是闭包//除了ext.xxx=value这种定义方法外,还可以使用ext{}这种书写方法。//ext{}不是ext(Closure)对应的函数调用。但是ext{}中的{}确实是闭包。getVersionNameAdvanced=this.&getVersionNameAdvanced}上面代码中有两个问题: 加载utils.gradle的Project对象和utils.gradle本身所代表的Script对象到底有什么关系? 我们在Groovy中也讲过怎么在一个Script中import另外一个Script中定义的类或者函数(见3.5脚本类、文件I/O和XML操作一节)。在Gradle中,这一块的处理比Groovy要复杂,具体怎么搞我还没完全弄清楚,但是Project和utils.gradle对于的Script的对象的关系是: 现在你知道问题1,2和答案了: 比如:我在posdevice每个build.gradle中都有如下的代码: tasks.getByName("assemble"){it.doLast{println"$project.name:Afterassemble,jarlibsarecopiedtolocalrepository"copyOutput(true)//copyOutput是utils.gradle输出的closure}}通过这种方式,我将一些常用的函数放到utils.gradle中,然后为加载它的Project设置ext属性。最后,Project中就可以调用这种赋值函数了! 注意:此处我研究的还不是很深,而且我个人感觉: 3.Task介绍 Task是Gradle中的一种数据类型,它代表了一些要执行或者要干的工作。不同的插件可以添加不同的Task。每一个Task都需要和一个Project关联。 关于Task。来看下面的例子: [build.gradle] //Task是和Project关联的,所以,我们要利用Project的task函数来创建一个TasktaskmyTask<==myTask是新建Task的名字taskmyTask{configureclosure}taskmyType<<{taskaction}<==注意,<<符号是doLast的缩写taskmyTask(type:SomeType)taskmyTask(type:SomeType){configureclosure}上述代码中都用了Project的一个函数,名为task,注意: 图32是Project中关于task函数说明: 陆陆续续讲了这么些内容,我自己感觉都有点烦了。是得,Gradle用一整本书来讲都嫌不够呢。 anyway,到目前为止,我介绍的都是一些比较基础的东西,还不是特别多。但是后续例子该涉及到的知识点都有了。下面我们直接上例子。这里有两个例子: 4.4.3posdevice实例 posdevice是一个multiproject。下面包含5个Project。对于这种Project,请大家回想下我们该创建哪些文件? 马上一个一个来看它们。 1.utils.gradle utils.gradle是我自己加的,为我们团队特意加了一些常见函数。主要代码如下: 2.settings.gradle 这个文件中我们该干什么?调用include把需要包含的子Project加进来。代码如下: [local.properties] local.dir=/home/innost/workspace/minsheng-flat-dir///注意,根据AndroidGradle的规范,只有下面两个属性是必须的,其余都是我自己加的sdk.dir=/home/innost/workspace/android-aosp-sdk/ndk.dir=/home/innost/workspace/android-aosp-ndk/debug.keystore=/home/innost/workspace/tools/mykeystore.jkssdk.api=android-19再次强调,sdk.dir和ndk.dir是AndroidGradle必须要指定的,其他都是我自己加的属性。当然。不编译ndk,就不需要ndk.dir属性了。 3.posdevicebuild.gradle 作为multi-project根目录,一般情况下,它的build.gradle是做一些全局配置。来看我的build.gradle [posdevicebuild.gradle] //下面这个subprojects{}就是一个ScriptBlocksubprojects{println"Configurefor$project.name"//遍历子Project,project变量对应每个子Projectbuildscript{//这也是一个SBrepositories{//repositories是一个SB///jcenter是一个函数,表示编译过程中依赖的库,所需的插件可以在jcenter仓库中//下载。jcenter()}dependencies{//SB//dependencies表示我们编译的时候,依赖android开发的gradle插件。插件对应的//classpath是com.android.tools.build。版本是1.2.3classpath'com.android.tools.build:gradle:1.2.3'}//为每个子Project加载utils.gradle。当然,这句话可以放到buildscript花括号之后applyfrom:rootProject.getRootDir().getAbsolutePath()+"/utils.gradle"}//buildscript结束}感觉解释得好苍白,SB在Gradle的API文档中也是有的。先来看Gradle定义了哪些SB。如图34所示: 你看,subprojects、dependencies、repositories都是SB。那么SB到底是什么?它是怎么完成所谓配置的呢? 仔细研究,你会发现SB后面都需要跟一个花括号,而花括号,恩,我们感觉里边可能一个Closure。由于图34说,这些SB的Description都有“Configurexxxforthisproject”,所以很可能subprojects是一个函数,然后其参数是一个Closure。是这样的吗? 特别提示:当你下次看到一个不认识的SB的时候,就去看API吧。 下面来解释代码中的各个SB: 4.CPosDeviceSdkbuild.gradle CPosDeviceSdk是一个AndroidLibrary。按Google的想法,AndroidLibrary编译出来的应该是一个AAR文件。但是我的项目有些特殊,我需要发布CPosDeviceSdk.jar包给其他人使用。jar在编译过程中会生成,但是它不属于AndroidLibrary的标准输出。在这种情况下,我需要在编译完成后,主动copyjar包到我自己设计的产出物目录中。 图36所示为Android的DSL参考信息。 图37为buildToolsVersion和compileSdkVersion的说明: 从图37可知,这两个变量是必须要设置的..... 5.CPosDeviceServerApkbuild.gradle 再来看一个APK的build,它包含NDK的编译,并且还要签名。根据项目的需求,我们只能签debug版的,而release版的签名得发布unsigned包给领导签名。另外,CPosDeviceServerAPK依赖CPosDeviceSdk。 虽然我可以先编译CPosDeviceSdk,得到对应的jar包,然后设置CPosDeviceServerApk直接依赖这个jar包就好。但是我更希望CPosDeviceServerApk能直接依赖于CPosDeviceSdk这个工程。这样,整个posdevice可以做到这几个Project的依赖关系是最新的。 在posdevice下执行gradleassemble命令,最终的输出文件都会拷贝到我指定的目录,结果如图38所示: 图38所示为posdevicegradleassemble的执行结果: 4.4.4实例2 下面这个实例也是来自一个实际的APP。这个APP对应的是一个单独的Project。但是根据我前面的建议,我会把它改造成支持Multi-ProjectsBuild的样子。即在工程目录下放一个settings.build。 另外,这个app有一个特点。它有三个版本,分别是debug、release和demo。这三个版本对应的代码都完全一样,但是在运行的时候需要从assets/runtime_config文件中读取参数。参数不同,则运行的时候会跳转到debug、release或者demo的逻辑上。 注意:我知道assets/runtime_config这种做法不decent,但,这是一个既有项目,我们只能做小范围的适配,而不是伤筋动骨改用更好的方法。另外,从未来的需求来看,暂时也没有大改的必要。 引入gradle后,我们该如何处理呢? 解决方法是:在编译build、release和demo版本前,在build.gradle中自动设置runtime_config的内容。代码如下所示: 几个问题,为什么我知道有preXXXBuild这样的任务? 答案:gradletasks--all查看所有任务。然后,多尝试几次,直到成功 taskmyTask<<{println'IammyTask'}书中说,如果代码没有加<<,则这个任务在脚本initialization(也就是你无论执行什么任务,这个任务都会被执行,IammyTask都会被输出)的时候执行,如果加了<<,则在gradlemyTask后才执行。 我开始完全不知道为什么,死记硬背。现在你明白了吗???? 这和我们调用task这个函数的方式有关!如果没有<<,则闭包在task函数返回前会执行,而如果加了<<,则变成调用myTask.doLast添加一个Action了,自然它会等到grdlemyTask的时候才会执行! 现在想起这个事情我还是很愤怒,API都说很清楚了......而且,如果你把Gradle当做编程框架来看,对于我们这些程序员来说,写这几百行代码,那还算是事嘛??