Gradle史上最详细解析AndroidM

构建,叫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中大量使用,比如很多类都定义了一些函数,这些函数最后一个参数都是一个闭包。比如:

publicstaticListeach(Listself,Closureclosure)上面这个函数表示针对List的每一个元素都会调用closure做一些处理。这里的closure,就有点回调函数的感觉。但是,在使用这个each函数的时候,我们传递一个怎样的Closure进去呢?比如:

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函数:

publicstaticListeach(Listself,Closureclosure)如何使用它呢?比如:

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当做编程框架来看,对于我们这些程序员来说,写这几百行代码,那还算是事嘛??

THE END
1.小学资料:中国菜的英文翻译中国菜的英文名1.北京烤鸭roastBeijingduck2.辣子鸡丁sautedicedchickenwithhotpeppers3.宫爆鸡丁sautedicedchickenwithpeanuts4.红烧鲤鱼braisedcommoncarp5.茄汁虾仁sautefishsliceswithbambooshoots6.涮羊肉instantboiledslicedmutton7.糖醋里脊porkfilletswithsweet&soursauce8.炒木须肉sauteshreddedporkwitheggs&blackfungus9.https://www.docin.com/touch_new/preview_new.do?id=4773975974
2.中国菜被翻译成这样吓得老外筷子都掉了!2.具有中国特色且已被国外主要英文字典收录的,使用汉语方言拼写或音译拼写的菜名,仍保留其原拼写方式。 如:豆腐 Tofu 宫保鸡丁 Kung Pao Chicken 馄饨Wonton 3. 中文菜肴名称无法体现其做法及主配料的,使用汉语拼音,并在后标注英文注释。 如:佛跳墙 Fotiahttps://mp.weixin.qq.com/s?__biz=Mzg2NDc0OTgyMg==&mid=2247548508&idx=3&sn=3e018031c427bef6107b07e3daeafef5&chksm=cfe4393558e43168ccd37ce3d279f1d10d67be4809131471bece7d8b9c91463c3daa9f64ab94&scene=27
3.粉条豆芽大虾白糖豆腐鸡肉宫保鸡丁十大硬菜料汁:1勺生抽+半勺蚝油+少许盐和鸡精+半勺白糖+1勺淀粉+半碗清水搅匀。 宫保鸡丁 家庭版宫保鸡丁做法简单又好吃,没一块鸡肉都裹满了浓郁的酱汁,肉质滑嫩入味,酸甜适中,配米饭简直不要太好吃。 香酥大虾 腌制方法:大虾处理干净+姜片+1勺生抽+1勺料酒+半勺蚝油+少许盐和胡椒粉抓匀腌制20分钟。 家常豆腐https://www.163.com/dy/article/JIGNHR400553TANT.html
4.正宗宫保鸡丁家常菜正宗宫保鸡丁 鸡胸肉1个 豆瓣酱3勺 白糖4大把 料酒1勺 黄瓜1根 胡萝卜1根 宫保鸡丁味道鲜美,甜而香! 步骤 第1步 首先把鸡胸肉切成2厘米的正方形。 第2步 然后,将切好的肉块用料酒淹5分钟。 第3步 然后,把肉块,放在油锅里炒,然后捞出肉块儿。 第4步 然后将肉块,胡萝卜,黄瓜,豆瓣儿酱一起放入锅https://www.coffee.cn/caipu/post/188868.html
5.宫保鸡丁的英文百度贴吧 聊兴趣,上贴吧 立即打开 百度贴吧内打开 继续访问 百度贴吧 聊兴趣 上贴吧 打开 chrome浏览器 继续 综合 贴 吧 人 直播 相关吧 查看更多 英语吧 关注150W 吧友互助吧 white水鸭8e86 求助宫保鸡丁的英文短语 分享回复赞 吉林宇诚出国吧 宇诚小小 吉林宇诚 教你如何翻译中国菜1、具有中国特色且被https://tieba.baidu.com/mo/q/hybrid/search?keyword=%E5%AE%AB%E4%BF%9D%E9%B8%A1%E4%B8%81%E7%9A%84%E8%8B%B1%E6%96%87
6.世界名牌與英文卡酥來砂鍋 cassoulet 宮保雞丁 kung pao chicken 法式清湯 consomme 北京烤鴨 peking duck 法式紅酒燉雞 coq au vin 蔥油餅 scallion pancakes 可麗餅 crepe 春捲 spring roll 法式烤起司火腿三明治 croque madame/monsieur 蒸餃 steamed dumplings 油封鴨 duck confit 糖醋排骨 sweet-and-sour pork 法國蝸牛 escargothttp://www.caneis.com.tw/link/outlet/outlet.html
7.宫保鸡丁做菜步骤ppt宫保鸡丁做菜步骤ppt由专业的PPT模板网站当图网提供,免费宫保鸡丁做菜步骤ppt下载,宫保鸡丁做菜步骤ppt免费下载,更多精美PPT模板,尽在当图网。https://www.99ppt.com/P_175504.html
8.16Langchain里的“记忆力”,让AI只记住有用的事儿llm_chain.predict(human_input="那宫保鸡丁呢?") llm_chain.predict(human_input="我问你的第一句话是什么?") 输出结果: ' 你是谁?' 再次询问第一句话是什么: llm_chain.predict(human_input="我问你的第一句话是什么?") 输出结果: ' 你问我的第一句话是“鱼香肉丝怎么做?”' https://blog.csdn.net/qq_37756660/article/details/135969439
9.在故宫东侧的北河沿大街,英文名Beiji在故宫东侧的北河沿大街,英文名Beijing Pie,低调又响亮的名字。上了必吃榜,更要来尝一尝。囧的是差点走错去了河沿春饼,肉饼在街更里面一些。 「河沿嫩香牛肉饼」招牌必点,现包现烙,端上来烫手,皮薄大馅,饼皮微焦,馅很香。「宫保鸡腿丁」和宫保鸡丁比,是块大肉嫩,酸甜中带着微辣,每种味道都不是很https://m.dianping.com/review/845708220
10.西餐文化范文12篇(全文)由于西餐厅大多讲究个性,一些菜的名字也起得有点让人不知所措,比如,他们常根据历史典故甚至本店厨师的经历来给菜起名字,这有点类似好多中国菜(如佛跳墙、宫保鸡丁等)被翻译成英文时的尴尬情况。例如,意大利有款非常美丽且好吃的点心竟然叫“苏格兰汤”……即使是常吃西餐的人,有时也会弄不清楚菜单上所写的料理https://www.99xueshu.com/w/ikeyg85b0pzq.html
11.方便食品超值商品网购打折优惠特价商品三明治,便当类 便利蜂的三明治,便当类有很多种,什么糖醋排骨,宫保鸡丁,寿司等等都有。这里的便当是需要买来自助加热的。便利蜂超市里有一个微波炉,专门加热各种便当,而且有专门的程序,一般1到3分钟就能加热好,很快。 阵主比较推荐他家的奥尔良鸡排三明治和糖醋里脊双拼饭。三明治里面加了满满的鸡肉,味道非常好;糖醋https://best.pconline.com.cn/fenlei/1245/shaiwu/
12.鸡胸肉怎样做菜好吃吃鸡肉的好处有哪些一、宫保鸡丁 材料:鸡胸肉200g,腰果1把,葱1棵,干辣椒10个,姜2片,蛋清1个,盐1.5茶匙,生抽1汤匙,老抽1/2汤匙,香醋1汤匙,糖2茶匙,鸡精1/2茶匙,淀粉2茶匙,胡椒粉1/2茶匙,骨头汤1汤匙。 做法: 1.葱切段,姜切丝。鸡胸肉用刀背拍一下,切成大拇指甲大小的丁。用蛋清,胡椒,盐半茶匙,腌渍十分钟入味。 https://m.120ask.com/news/p/328442
13.熹妃传厨房做菜需要什么食材熹妃传厨房做菜菜谱介绍【宫保鸡丁】油/盐/糖酱/醋(鸡又8 花生16 胡萝卜10 青椒10) 【湘西酸肉】油/盐/酱(猪又15 青椒10 红椒10) 【菌菇煲】水/油/盐/水(蘑菇20 鸡蛋10 大蒜10) 【杭州煨鸡】油/盐/水/糖/水(猪又5 鸡又20 蘑菇15) 【贵妃鸡翅】油/盐/酱/糖(鸡又45 红椒20) https://www.qqtn.com/article/article_279158_1.html
14.宫保鸡丁怎么做宫保鸡丁的做法这是我第一次制作宫保鸡丁,在做菜过程中忘记拍照了,所以想用文字记述下来,以后的每一道菜,我都会和大家分享并且记录具体步骤,请大家多多关注哟 宫保鸡丁的做法 步骤1 首先:鸡胸肉 黄瓜 均匀切成小块 大葱 和蒜头 切碎 步骤2 切好的鸡胸肉块放入碗内,并且依次放入 : 食用盐 、黑胡椒粉、五香粉、一滴老抽酱油https://www.douguo.com/cookbook/1531640.html
15.宫保鸡丁的做法菜谱宫保鸡丁 宫保鸡丁的做法 材料图,生抽10克 料酒10克 醋10克兑汁,加淀粉拌成芡汁。 纳碗加料酒生抽盐胡椒粉蛋清抓均匀,加少量干淀粉抓均匀。 4.胡萝卜 、黄瓜切丁,葱白切厘米段。 5.炒锅倒油润锅后加油,四成热时倒入腌好的鸡丁滑油。 6. 7.锅中留下小量烹调油,炒香花椒和辣椒段,加豆瓣炒香https://www.xiangha.com/caipu/19349145.html
16.2024大模型应用宝典.docx3.当我们直接问,“那宫保鸡丁呢?”,AI会正确回答出宫保鸡丁的做法。输出结果:而如果我们没有加上AI总结的之前的对话,只是让AI对话,它只能和你闲扯一些别的。1conversation=Conversation( 输出结果: 然后,我们再新建一个Conversation,这次的提示语里,我们先加上了总结的内容,然后 告诉AI把对话继续下去。 2. 注意https://max.book118.com/html/2024/0904/8016135125006123.shtm
17.老实说因为是南方人我没吃过特别正宗的宫保鸡丁做菜全靠好吃到停不下来的宫保鸡丁(有教程超简单哦) 宫保鸡丁 老实说 因为是南方人 我没吃过特别正宗的宫保鸡丁 做菜全靠网上找菜谱以及想象所以要有哪里做的不正宗的地方大家多多包涵毕竟做菜和生活一样 没有什么规则与定律 想怎么做全凭心情好吃就行了嘛对不对~ https://m.zhuxiaobang.com/weitoutiao/1642464724288515