原文地址:深入理解Android(一):Gradle详解-InfoQ
备用地址:Gradle史上最详细解析 - AndroidM - 博客园
以下内容为复制-粘贴,建议看原文。
编者按:随着移动设备硬件能力的提升,Android 系统开放的特质开始显现,各种开发的奇技淫巧、黑科技不断涌现,InfoQ 特联合《深入理解 Android》系列图书作者邓凡平,开设深入理解Android 专栏,探索Android 从框架到应用开发的奥秘。
Gradle 是当前非常“劲爆”的构建工具。本篇文章就是专为讲解 Gradle 而来。介绍 Gradle 之前,先说点题外话。
说实话,我在索尼工作的时候,就见过 Gradle。但是当时我一直不知道这是什么东西。而且索尼工具组的工程师还将其和 Android Studio 索尼版一起推送,偶一看就更没兴趣了。为什么那个时候如此不待见 Gradle 呢?因为我此前一直是做 ROM 开发。在这个层面上,我们用 make,mm 或者 mmm 就可以了。而且,编译耗时对我们来说也不是啥痛点,因为用组内吊炸天的神机服务器完整编译索尼的 image 也要耗费 1 个小时左右。所以,那个时侯 Gradle 完全不是我们的菜。
现在,搞 APP 开发居多,编译 / 打包等问题立即就成痛点了。比如:
上述问题对绝大部分 APP 开发者而言都不陌生,而Gradle作为一种很方便的的构建工具,可以非常轻松得解决构建过程中的各种问题。
构建,叫 build 也好,叫 make 也行。反正就是根据输入信息然后干一堆事情,最后得到几个产出物(Artifact)。
最最简单的构建工具就是 make 了。make 就是根据 Makefile 文件中写的规则,执行对应的命令,然后得到目标产物。
日常生活中,和构建最类似的一个场景就是做菜。输入各种食材,然后按固定的工序,最后得到一盘菜。当然,做同样一道菜,由于需求不同,做出来的东西也不尽相同。比如,宫保鸡丁这道菜,回民要求不能放大油、口淡的要求少放盐和各种油、辣不怕的男女汉子们可以要求多放辣子…总之,做菜包含固定的工序,但是对于不同条件或需求,需要做不同的处理。
在 Gradle 爆红之前,常用的构建工具是 ANT,然后又进化到 Maven。ANT 和 Maven 这两个工具其实也还算方便,现在还有很多地方在使用。但是二者都有一些缺点,所以让更懒得人觉得不是那么方便。比如,Maven 编译规则是用 XML 来编写的。XML 虽然通俗易懂,但是很难在 xml 中描述 **if{某条件成立,编译某文件}/else{编译其他文件}** 这样有不同条件的任务。
怎么解决?怎么解决好?对程序员而言,自然是编程解决,但是有几个小要求:
土匪:蘑菇,你哪路?什么价?(什么人?到哪里去?)
杨子荣:哈!想啥来啥,想吃奶来了妈妈,想娘家的人,孩子他舅舅来了。(找同行)
杨子荣:拜见三爷!
土匪:天王盖地虎!(你好大的胆!敢来气你的祖宗?)
杨子荣:宝塔镇河妖!(要是那样,叫我从山上摔死,掉河里淹死。)
土匪:野鸡闷头钻,哪能上天王山!(你不是正牌的。)
杨子荣:地上有的是米,喂呀,有根底!(老子是正牌的,老牌的。)
Gradle 中也有类似的行话,比如 sourceSets 代表源文件的集合等…太多了,记不住。以后我们都会接触到这些行话。那么,对使用者而言,这些行话的好处是什么呢?这就是:
一句行话可以包含很多意思,而且在这个行当里的人一听就懂,不用解释。另外,基于行话,我们甚至可以建立一个模板,使用者只要往这个模板里填必须要填的内容,Gradle 就可以非常漂亮得完成工作,得到想要的东西。
这就和现在的智能炒菜机器似的,只要选择菜谱,把食材准备好,剩下的事情就不用你操心了。吃货们对这种做菜方式肯定是以反感为主,太没有特色了。但是程序员对 Gradle 类似做法却热烈拥抱。
到此,大家应该明白要真正学会 Gradle 恐怕是离不开下面两个基础知识:
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 之间的关系。
实际上,由于 Groovy Code 在真正执行的时候已经变成了 Java 字节码,所以 JVM 根本不知道自己运行的是 Groovy 代码。
下面我们将介绍 Groovy。由于此文的主要目的是 Gradle,所以我们不会过多讨论 Groovy 中细枝末节的东西,而是把知识点集中在以后和 Gradle 打交道时一些常用的地方上。
在学习本节的时候,最好部署一下 Groovy 开发环境。根据 Groovy 官网的介绍,部署 Groovy 开发环境非常简单,在 Ubuntu 或者 cygwin 之类的地方:
然后,创建一个 test.groovy 文件,里边只有一行代码:
println "hello groovy"
- 执行 groovy test.groovy,输出结果如图 2 所示:
亲们,必须要完成上面的操作啊。做完后,有什么感觉和体会?
最大的感觉可能就是 groovy 和 shell 脚本,或者 python 好类似。
另外,除了可以直接使用 JDK 之外,Groovy 还有一套 GDK 。
说实话,看了这么多家 API 文档,还是 Google 的 Android API 文档做得好。其页面中右上角有一个搜索栏,在里边输入一些关键字,瞬间就能列出候选类,相关文档,方便得不得了啊…
为了后面讲述方面,这里先介绍一些前提知识。初期接触可能有些别扭,看习惯就好了。
复制代码
|
|
|
|
|
复制代码
|
|
|
|
|
// 无类型的函数定义,必须使用 def 关键字
复制代码
|
|
|
|
|
|
|
|
|
|
|
|
|
其实,所谓的无返回类型的函数,我估计内部都是按返回 Object 类型来处理的。毕竟,Groovy 是基于 Java 的,而且最终会转成 Java Code 运行在 JVM 上
复制代码
|
|
|
|
|
|
|
|
|
注意,如果函数定义时候指明了返回值类型的话,函数中则必须返回正确的数据类型,否则运行时报错。如果使用了动态类型的话,你就可以返回任何类型了。
1 单引号’'中的内容严格对应 Java 中的 String,不对 $ 符号进行转义
def singleQuote='I am $ dolloar' // 输出就是 I am $ dolloar
2 双引号""的内容则和脚本语言的处理有点像,如果字符中有 $ 号的话,则它会$ 表达式先求值。
复制代码
|
|
|
|
|
3 三个引号’’‘xxx’’'中的字符串支持随意换行 比如
复制代码
|
|
|
|
|
|
|
println("test") ---> println "test"
注意,虽然写代码的时候,对于函数调用可以不带括号,但是 Groovy 经常把属性和函数调用混淆。比如
复制代码
|
|
|
|
|
getSomething() // 如果不加括号的话,Groovy 会误认为 getSomething 是一个变量。
所以,调用函数要不要带括号,我个人意见是如果这个函数是 Groovy API 或者 Gradle API 中比较常用的,比如 println,就可以不带括号。否则还是带括号。Groovy 自己也没有太好的办法解决这个问题,只能兵来将挡水来土掩了。
好了,了解上面一些基础知识后,我们再介绍点深入的内容。
Groovy 中的数据类型我们就介绍两种和 Java 不太一样的:
放心,这里介绍的东西都很简单
3.3.1 基本数据类型
作为动态语言,Groovy 世界中的所有事物都是对象。所以,int,boolean 这些 Java 中的基本数据类型,在 Groovy 代码中其实对应的是它们的包装数据类型。比如 int 对应为 Integer,boolean 对应为 Boolean。比如下图中的代码执行结果:
图 4 int 实际上是 Integer
3.3.2 容器类
Groovy 中的容器类很简单,就三种:
对容器而言,我们最重要的是了解它们的用法。下面是一些简单的例子:
1. List 类
复制代码
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2. Map 类
复制代码
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3. Range 类
Range 是 Groovy 对 List 的一种拓展,变量定义和大体的使用方法如下:
复制代码
|
|
|
|
|
|
|
|
|
|
|
3.3.4 Groovy API 的一些秘笈
前面讲这些东西,主要是让大家了解 Groovy 的语法。实际上在 coding 的时候,是离不开 SDK 的。由于 Groovy 是动态语言,所以要使用它的 SDK 也需要掌握一些小诀窍。
Groovy 的 API 文档位于 The Apache Groovy programming language - Groovy Development Kit
以上文介绍的 Range 为例,我们该如何更好得使用它呢?
有了 API 文档,你就可以放心调用其中的函数了。不过,不过,不过:我们刚才代码中用到了 Range.from/to 属性值,但翻看 Range API 文档的时候,其实并没有这两个成员变量。图 6 是 Range 的方法
文档中并没有说明 Range 有 from 和 to 这两个属性,但是却有 getFrom 和 getTo 这两个函数。What happened?原来:
根据 Groovy 的原则,如果一个类中有名为 xxyyzz 这样的属性(其实就是成员变量),Groovy 会自动为它添加 getXxyyzz 和 setXxyyzz 两个函数,用于获取和设置 xxyyzz 属性值。
注意,get 和 set 后第一个字母是大写的
所以,当你看到 Range 中有 getFrom 和 getTo 这两个函数时候,就得知道潜规则下,Range 有 from 和 to 这两个属性。当然,由于它们不可以被外界设置,所以没有公开 setFrom 和 setTo 函数。
3.4.1 闭包的样子
闭包,英文叫 Closure,是 Groovy 中非常重要的一个数据类型或者说一种概念了。闭包的历史来源,种种好处我就不说了。我们直接看怎么使用它!
闭包,是一种数据类型,它代表了一段可执行的代码。其外形如下:
复制代码
|
|
|
|
|
|
|
|
|
简而言之,Closure 的定义格式是:
复制代码
|
|
|
说实话,从 C/C++ 语言的角度看,闭包和函数指针很像。闭包定义好后,要调用它的方法就是:
闭包对象.call(参数) 或者更像函数指针调用的方法:
闭包对象 (参数)
比如:
复制代码
|
|
|
上面就是一个闭包的定义和使用。在闭包中,还需要注意一点:
如果闭包没定义参数的话,则隐含有一个参数,这个参数名字叫 it,和 this 的作用类似。it 代表闭包的参数。
比如:
复制代码
|
|
|
等同于:
复制代码
|
|
|
但是,如果在闭包定义时,采用下面这种写法,则表示闭包没有参数!
def noParamClosure = { -> true }
这个时候,我们就不能给 noParamClosure 传参数了!
noParamClosure ("test") <== 报错喔!
3.4.2 Closure 使用中的注意点
1. 省略圆括号
闭包在 Groovy 中大量使用,比如很多类都定义了一些函数,这些函数最后一个参数都是一个闭包。比如:
public static
上面这个函数表示针对 List 的每一个元素都会调用 closure 做一些处理。这里的 closure,就有点回调函数的感觉。但是,在使用这个 each 函数的时候,我们传递一个怎样的 Closure 进去呢?比如:
复制代码
|
|
|
|
|
|
|
上面代码有两个知识点:
复制代码
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
注意,这个特点非常关键,因为以后在 Gradle 中经常会出现图 7 这样的代码:
经常碰见图 7 这样的没有圆括号的代码。省略圆括号虽然使得代码简洁,看起来更像脚本语言,但是它这经常会让我 confuse(不知道其他人是否有同感),以 doLast 为例,完整的代码应该按下面这种写法:
复制代码
|
|
|
|
|
有了圆括号,你会知道 doLast 只是把一个 Closure 对象传了进去。很明显,它不代表这段脚本解析到 doLast 的时候就会调用 println ‘Hello world!’ 。
但是把圆括号去掉后,就感觉好像 println 'Hello world!'立即就会被调用一样!
2. 如何确定 Closure 的参数
另外一个比较让人头疼的地方是,Closure 的参数该怎么搞?还是刚才的 each 函数:
public static
如何使用它呢?比如:
复制代码
|
|
|
|
|
|
|
看起来很轻松,其实:
我们能写成下面这样吗?
复制代码
|
|
|
|
|
所以,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 所示:
你看,图 10 中的 Test.groovy 和 Java 类就很相似了。当然,如果不声明 public/private 等访问权限的话,Groovy 中类及其变量默认都是 public 的。
现在,我们在测试的根目录下建立一个 test.groovy 文件。其代码如下所示:
你看,test.groovy 先 import 了 com.cmbc.groovy.Test 类,然后创建了一个 Test 类型的对象,接着调用它的 print 函数。
这两个 groovy 文件的目录结构如图 12 所示:
在 groovy 中,系统自带会加载当前目录 / 子目录下的 xxx.groovy 文件。所以,当执行 groovy test.groovy 的时候,test.groovy import 的 Test 类能被自动搜索并加载到。
2. 脚本到底是什么
Java 中,我们最熟悉的是类。但是我们在 Java 的一个源码文件中,不能不写 class(interface 或者其他…),而 Groovy 可以像写脚本一样,把要做的事情都写在 xxx.groovy 中,而且可以通过 groovy xxx.groovy 直接执行这个脚本。这到底是怎么搞的?
既然是基于 Java 的,Groovy 会先把 xxx.groovy 中的内容转换成一个 Java 类。比如:
test.groovy 的代码是:
println 'Groovy world!'
Groovy 把它转换成这样的 Java 类:
执行 groovyc -d classes test.groovy
groovyc是 groovy 的编译命令,-d classes 用于将编译得到的 class 文件拷贝到 classes 文件夹下
图 13 是 test.groovy 脚本转换得到的 java class。用 jd-gui 反编译它的代码:
图 13 中:
groovyc是一个比较好的命令,读者要掌握它的用法。然后利用 jd-gui 来查看对应 class 的 Java 源码。
3. 脚本中的变量和作用域
前面说了,xxx.groovy 只要不是和 Java 那样的 class,那么它就是一个脚本。而且脚本的代码其实都会被放到 run 函数中去执行。那么,在 Groovy 的脚本中,很重要的一点就是脚本中定义的变量和它的作用域。举例:
复制代码
|
|
|
|
|
|
|
printx() <== 报错,说 x 找不到
为什么?继续来看反编译后的 class 文件。
图 14 中:
那么,如何使得 printx 能访问 x 呢?很简单,定义的时候不要加类型和 def。即:
复制代码
|
|
|
|
|
|
|
|
|
这次 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 中。
执行 groovy test1.groovy,报错。说 x 找不到。这是因为 x 是在 test 的 run 函数动态加进去的。怎么办?
复制代码
|
|
|
查看编译后的 test.class 文件,得到:
这个时候,test.groovy 中的 x 就成了 test 类的成员函数了。如此,我们可以在 script 中定义那些需要输出给外部脚本或类使用的变量了!
3.5.2 文件 I/O 操作
本节介绍下 Groovy 的文件 I/O 操作。直接来看例子吧,虽然比 Java 看起来简单,但要理解起来其实比较难。尤其是当你要自己查 SDK 并编写代码的时候。
整体说来,Groovy 的 I/O 操作是在原有 Java I/O 操作上进行了更为简单方便的封装,并且使用 Closure 来简化代码编写。主要封装了如下一些了类:
Groovy 中,文件读操作简单到令人发指:
def targetFile = new File(文件名) <==File 对象还是要创建的。
然后打开 http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/File.html
看看 Groovy 定义的 API:
1 读该文件中的每一行:eachLine 的唯一参数是一个 Closure。Closure 的参数是文件每一行的内容
其内部实现肯定是 Groovy 打开这个文件,然后读取文件的一行,然后调用 Closure…
复制代码
|
|
|
|
|
|
|
2 直接得到文件内容
targetFile.getBytes() <== 文件内容一次性读出,返回类型为 byte[]
注意前面提到的 getter 和 setter 函数,这里可以直接使用 targetFile.bytes //…
3 使用 InputStream.InputStream 的 SDK 在 InputStream (Groovy JDK enhancements)
复制代码
|
|
|
|
|
4 使用闭包操作 inputStream,以后在 Gradle 里会常看到这种搞法
复制代码
|
|
|
|
|
确实够简单,令人发指。我当年死活也没找到 withInputStream 是个啥意思。所以,请各位开发者牢记 Groovy I/O 操作相关类的 SDK 地址:
2. 写文件
和读文件差不多。不再啰嗦。这里给个例子,告诉大家如何 copy 文件。
复制代码
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
关于 OutputStream 的 << 操作符重载,查看 SDK 文档后可知:
再一次向极致简单致敬。但是,SDK 恐怕是离不开手了…
3.5.3 XML 操作
除了 I/O 异常简单之外,Groovy 中的 XML 操作也极致得很。Groovy 中,XML 的解析提供了和 XPath 类似的方法,名为 GPath。这是一个类,提供相应 API。关于 XPath,请看 Wiki 。
GPath 功能包括:给个例子好了,来自 Groovy 官方文档。
test.xml 文件:
复制代码
已复制
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
复制代码
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
作为一门语言,Groovy 是复杂的,是需要深入学习和钻研的。一本厚书甚至都无法描述 Groovy 的方方面面。
Anyway,从使用角度看,尤其是又限定在 Gradle 这个领域内,能用到的都是 Groovy 中一些简单的知识。
现在正式进入 Gradle。Gradle 是一个工具,同时它也是一个编程框架。前面也提到过,使用这个工具可以完成 app 的编译打包等工作。当然你也可以用它干其他的事情。
Gradle 是什么?学习它到什么地步就可以了?
=====> 看待问题的时候,所站的角度非常重要。
–> 当你把 Gradle 当工具看的时候,我们只想着如何用好它。会写、写好配置脚本就 OK
–> 当你把它当做编程框架看的时候,你可能需要学习很多更深入的内容。
另外,今天我们把它当工具看,明天因为需求发生变化,我们可能又得把它当编程框架看。
Gradle 的官网: http://gradle.org/
文档位置: https://docs.gradle.org/current/release-notes。其中的 ** User Guide和DSL Reference** 很关键。User Guide 就是介绍 Gradle 的一本书,而 DSL Reference 是 Gradle API 的说明。
以 Ubuntu 为例,下载 Gradle: http://gradle.org/gradle-download/ 选择Complete distribution和Binary only distribution都行。然后解压到指定目录。
最后,设置~/.bashrc,把 Gradle 加到 PATH 里,如图 20 所示:
执行 source ~/.bashrc,初始化环境。
执行 gradle --version,如果成功运行就 OK 了。
注意,为什么说 Gradle 是一个编程框架?来看它提供的 API 文档:
Project (Gradle API 7.2)
原来,我们编写所谓的编译脚本,其实就是玩 Gradle 的 API…所以它从更底层意义上看,是一个编程框架!
既然是编程框架,我在讲解 Gradle 的时候,尽量会从 API 的角度来介绍。有些读者肯定会不耐烦,为嘛这么费事?
从我个人的经历来看:因为我从网上学习到的资料来看,几乎全是从脚本的角度来介绍 Gradle,结果学习一通下来,只记住参数怎么配置,却不知道它们都是函数调用,都是严格对应相关 API 的。
而从 API 角度来看待 Gradle 的话,有了 SDK 文档,你就可以编程。编程是靠记住一行行代码来实现的吗?不是,是在你掌握大体流程,然后根据 SDK+API 来完成的!
其实,Gradle 自己的 User Guide 也明确说了:
Build scripts are code
Gradle 是一个框架,它定义一套自己的游戏规则。我们要玩转 Gradle,必须要遵守它设计的规则。下面我们来讲讲 Gradle 的基本组件:
Gradle 中,每一个待编译的工程都叫一个 Project。每一个 Project 在构建的时候都包含一系列的 Task。比如一个 Android APK 的编译可能包含:Java 源码编译 Task、资源编译 Task、JNI 编译 Task、lint 检查 Task、打包生成 APK 的 Task、签名 Task 等。
一个 Project 到底包含多少个 Task,其实是由编译脚本指定的插件决定。插件是什么呢?插件就是用来定义 Task,并具体执行这些 Task 的东西。
刚才说了,Gradle 是一个框架,作为框架,它负责定义流程和规则。而具体的编译工作则是通过插件的方式来完成的。比如编译 Java 有 Java 插件,编译 Groovy 有 Groovy 插件,编译 Android APP 有 Android APP 插件,编译 Android Library 有 Android Library 插件
好了。到现在为止,你知道 Gradle 中每一个待编译的工程都是一个 Project,一个具体的编译过程是由一个一个的 Task 来定义和执行的。
4.2.1 一个重要的例子
下面我们来看一个实际的例子。这个例子非常有代表意义。图 22 是一个名为 posdevice 的目录。这个目录里包含 3 个 Android Library 工程,2 个 Android APP 工程。
在图 22 的例子中:
请回答问题,在上面这个例子中,有多少个 Project?
答案是:每一个 Library 和每一个 App 都是单独的 Project。根据 Gradle 的要求,每一个 Project 在其根目录下都需要有一个 build.gradle。build.gradle 文件就是该 Project 的编译脚本,类似于 Makefile。
看起来好像很简单,但是请注意:posdevice 虽然包含 5 个独立的 Project,但是要独立编译他们的话,得:
这很麻烦啊,有 10 个独立 Project,就得重复执行 10 次这样的命令。更有甚者,所谓的独立 Project 其实有依赖关系的。比如我们这个例子。
那么,我想在 posdevice 目录下,直接执行 gradle assemble,是否能把这 5 个 Project 的东西都编译出来呢?
答案自然是可以。在 Gradle 中,这叫Multi-Projects Build。把 posdevice 改造成支持 Gradle 的 Multi-Projects Build 很容易,需要:
来看 settings.gradle 的内容,最关键的内容就是告诉 Gradle 这个 multiprojects 包含哪些子 projects:
[settings.gradle]
复制代码
|
|
|
|
|
强烈建议:
如果你确实只有一个 Project 需要编译,我也建议你在目录下添加一个 settings.gradle。我们团队内部的所有单个 Project 都已经改成支持 Multiple-Project Build 了。改得方法就是添加 settings.gradle,然后 include 对应的 project 名字。
另外,settings.gradle 除了可以 include 外,还可以设置一些函数。这些函数会在 gradle 构建整个工程任务的时候执行,所以,可以在 settings 做一些初始化的工作。比如:我的 settings.gradle 的内容:
复制代码
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4.2.2 gradle 命令介绍
1. gradle projects 查看工程信息
到目前为止,我们了解了 Gradle 什么呢?
gradle 提供一些方便命令来查看和 Project,Task 相关的信息。比如在 posdevice 中,我想看这个 multi projects 到底包含多少个子 Project:
执行 gradle projects,得到图 23:
你看,multi projects 的情况下,posdevice 这个目录对应的 build.gradle 叫 Root Project,它包含 5 个子 Project。
如果你修改 settings.gradle,使得 include 只有一个参数,则 gradle projects 的子 project 也会变少,比如图 24:
查看了 Project 信息,这个还比较简单,直接看 settings.gradle 也知道。那么 Project 包含哪些 Task 信息,怎么看呢?图 23,24 中最后的输出也告诉你了,想看某个 Project 包含哪些 Task 信息,只要执行:
gradle project-path:tasks 就行。注意,project-path是目录名,后面必须跟冒号。
对于 Multi-project,在根目录中,需要指定你想看哪个 poject 的任务。不过你要是已经 cd 到某个 Project 的目录了,则不需指定 Project-path。
来看图 25:
图 25 是 gradle CPosSystemSdk:tasks 的结果。
CPosSystemSdk 是一个 Android Library 工程,Android Library 对应的插件定义了好多 Task。每种插件定义的 Task 都不尽相同,这就是所谓的 Domain Specific,需要我们对相关领域有比较多的了解。
这些都是后话,我们以后会详细介绍。
3. gradle task-name 执行任务
图 25 中列出了好多任务,这时候就可以通过 gradle 任务名来执行某个任务。这和 make xxx 很像。比如:
gradle tasks 会列出每个任务的描述,通过描述,我们大概能知道这些任务是干什么的…。然后 gradle task-name 执行它就好。
这里要强调一点:Task 和 Task 之间往往是有关系的,这就是所谓的依赖关系。比如,assemble task 就依赖其他 task 先执行,assemble 才能完成最终的输出。
依赖关系对我们使用 gradle 有什么意义呢?
如果知道 Task 之间的依赖关系,那么开发者就可以添加一些定制化的 Task。比如我为 assemble 添加一个 SpecialTest 任务,并指定 assemble 依赖于 SpecialTest。当 assemble 执行的时候,就会先处理完它依赖的 task。自然,SpecialTest 就会得到执行了…
大家先了解这么多,等后面介绍如何写 gradle 脚本的时候,这就是调用几个函数的事情,Nothing Special!
Gradle 的工作流程其实蛮简单,用一个图 26 来表达:
图 26 告诉我们,Gradle 工作包含三个阶段:
我在:
好了,Hook 的代码怎么写,估计你很好奇,而且肯定会埋汰,怎么就还没告诉我怎么写 Gradle。马上了!
最后,关于 Gradle 的工作流程,你只要记住:
下面来告诉你怎么写代码!
希望你在进入此节之前,一定花时间把前面内容看一遍!!!
https://docs.gradle.org/current/dsl/ <== 这个文档很重要
Gradle 基于 Groovy,Groovy 又基于 Java。所以,Gradle 执行的时候和 Groovy 一样,会把脚本转换成 Java 对象。Gradle 主要有三种对象,这三种对象和三种不同的脚本文件对应,在 gradle 执行的时候,会将脚本转换成对应的对端:
注意,对于其他 gradle 文件,除非定义了 class,否则会转换成一个实现了 Script 接口的对象。这一点和 3.5 节中 Groovy 的脚本类相似
当我们执行 gradle 的时候,gradle 首先是按顺序解析各个 gradle 文件。这里边就有所所谓的生命周期的问题,即先解析谁,后解析谁。图 27 是 Gradle 文档中对生命周期的介绍:结合上一节的内容,相信大家都能看明白了。现在只需要看红框里的内容:
4.4.1 Gradle 对象
我们先来看 Gradle 对象,它有哪些属性呢?如图 28 所示:
我在 posdevice build.gradle 中和 settings.gradle 中分别加了如下输出:
复制代码
|
|
|
|
|
|
|
|
|
得到结果如图 29 所示:
Gradle 的函数接口在文档中也有。
4.4.2 Project 对象
每一个 build.gradle 文件都会转换成一个 Project 对象。在 Gradle 术语中,Project 对象对应的是Build Script。
Project 包含若干 Tasks。另外,由于 Project 对应具体的工程,所以需要为 Project 加载所需要的插件,比如为 Java 工程加载 Java 插件。其实,一个 Project 包含多少 Task 往往是插件决定的。
所以,在 Project 中,我们要:
1. 加载插件
Project 的 API 位于 https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html 。加载插件是调用它的 apply 函数.apply 其实是 Project 实现的 PluginAware 接口定义的:
来看代码:
[apply 函数的用法]
apply 是一个函数,此处调用的是图 30 中最后一个 apply 函数。注意,Groovy 支持函数调用的时候通过 参数名 1: 参数值 2,参数名 2:参数值 2 的方式来传递参数
复制代码
|
|
|
除了加载二进制的插件(上面的插件其实都是下载了对应的 jar 包,这也是通常意义上我们所理解的插件),还可以加载一个 gradle 文件。为什么要加载 gradle 文件呢?
其实这和代码的模块划分有关。一般而言,我会把一些通用的函数放到一个名叫 utils.gradle 文件里。然后在其他工程的 build.gradle 来加载这个 utils.gradle。这样,通过一些处理,我就可以调用 utils.gradle 中定义的函数了。
加载 utils.gradle 插件的代码如下:
utils.gradle 是我封装的一个 gradle 脚本,里边定义了一些方便函数,比如读取 AndroidManifest.xml 中
的 versionName,或者是 copy jar 包 /APK 包到指定的目录
apply from: rootProject.getRootDir().getAbsolutePath() + "/utils.gradle"
也是使用 apply 的最后一个函数。那么,apply 最后一个函数到底支持哪些参数呢?还是得看图 31 中的 API 说明:
我这里不遗余力的列出 API 图片,就是希望大家在写脚本的时候,碰到不会的,一定要去查看 API 文档!
2. 设置属性
如果是单个脚本,则不需要考虑属性的跨脚本传播,但是 Gradle 往往包含不止一个 build.gradle 文件,比如我设置的 utils.gradle,settings.gradle。如何在多个脚本中设置属性呢?
Gradle 提供了一种名为extra property的方法。extra property是额外属性的意思,在第一次定义该属性的时候需要通过 ext 前缀来标示它是一个额外的属性。定义好之后,后面的存取就不需要 ext 前缀了。ext 属性支持 Project 和 Gradle 对象。即 Project 和 Gradle 对象都可以设置 ext 属性
举个例子:
我在 settings.gradle 中想为 Gradle 对象设置一些外置属性,所以在 initMinshengGradleEnvironment 函数中
复制代码
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
再来一个例子强化一下:
我在 utils.gradle 中定义了一些函数,然后想在其他 build.gradle 中调用这些函数。那该怎么做呢?
[utils.gradle]
复制代码
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
上面代码中有两个问题:
project 是谁?
ext 是谁的 ext?
上面两个问题比较关键,我也是花了很长时间才搞清楚。这两个问题归结到一起,其实就是:
加载 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 中都有如下的代码:
复制代码
|
|
|
|
|
|
|
|
|
|
|
通过这种方式,我将一些常用的函数放到 utils.gradle 中,然后为加载它的 Project 设置 ext 属性。最后,Project 中就可以调用这种赋值函数了!
注意:此处我研究的还不是很深,而且我个人感觉:
3. Task 介绍
Task 是 Gradle 中的一种数据类型,它代表了一些要执行或者要干的工作。不同的插件可以添加不同的 Task。每一个 Task 都需要和一个 Project 关联。
Task 的 API 文档位于 https://docs.gradle.org/current/dsl/org.gradle.api.Task.html 。关于 Task,我这里简单介绍下 build.gradle 中怎么写它,以及 Task 中一些常见的类型
关于 Task。来看下面的例子:
[build.gradle]
复制代码
|
|
|
|
|
|
|
|
|
|
|
上述代码中都用了 Project 的一个函数,名为 task,注意:
图 32 是 Project 中关于 task 函数说明:
陆陆续续讲了这么些内容,我自己感觉都有点烦了。是得,Gradle 用一整本书来讲都嫌不够呢。
anyway,到目前为止,我介绍的都是一些比较基础的东西,还不是特别多。但是后续例子该涉及到的知识点都有了。下面我们直接上例子。这里有两个例子:
4.4.3 posdevice 实例
现在正是开始通过例子来介绍怎么玩 gradle。这里要特别强调一点,根据 Gradle 的哲学。gradle 文件中包含一些所谓的Script Block(姑且这么称它)。Script Block作用是让我们来配置相关的信息。不同的SB有不同的需要配置的东西。这也是我最早说的行话。比如,源码对应的 SB,就需要我们配置源码在哪个文件夹里。关于 SB,我们后面将见识到!
posdevice 是一个 multi project。下面包含 5 个 Project。对于这种 Project,请大家回想下我们该创建哪些文件?
马上一个一个来看它们。
1. utils.gradle
utils.gradle 是我自己加的,为我们团队特意加了一些常见函数。主要代码如下:
[utils.gradle]
复制代码
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
图 33 展示了被 disable 的 Debug 任务的部分信息:
这个文件中我们该干什么?调用 include 把需要包含的子 Project 加进来。代码如下:
[settings.gradle]
复制代码
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
注意,对于 Android 来说,local.properties 文件是必须的,它的内容如下:
[local.properties]
复制代码
|
|
|
|
|
|
|
|
|
|
|
再次强调,sdk.dir和ndk.dir是 Android Gradle 必须要指定的,其他都是我自己加的属性。当然。不编译ndk,就不需要ndk.dir属性了。
3. posdevice build.gradle
作为 multi-project 根目录,一般情况下,它的 build.gradle 是做一些全局配置。来看我的 build.gradle
[posdevice build.gradle]
复制代码
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
感觉解释得好苍白,SB 在 Gradle 的 API 文档中也是有的。先来看 Gradle 定义了哪些 SB。如图 34 所示:
你看,subprojects、dependencies、repositories 都是 SB。那么 SB 到底是什么?它是怎么完成所谓配置的呢?
仔细研究,你会发现 SB 后面都需要跟一个花括号,而花括号,恩,我们感觉里边可能一个 Closure。由于图 34 说,这些 SB 的 Description 都有“Configure xxx for this project”,所以很可能 subprojects 是一个函数,然后其参数是一个 Closure。是这样的吗?
Absolutely right。只是这些函数你直接到 Project API 里不一定能找全。不过要是你好奇心重,不妨到 https://docs.gradle.org/current/javadoc/ ,选择Index这一项,然后ctrl+f,输入图 34 中任何一个 Block,你都会找到对应的函数。比如我替你找了几个 API,如图 35 所示:
特别提示:当你下次看到一个不认识的 SB 的时候,就去看 API 吧。
下面来解释代码中的各个 SB:
4. CPosDeviceSdk build.gradle
CPosDeviceSdk 是一个 Android Library。按 Google 的想法,Android Library 编译出来的应该是一个 AAR 文件。但是我的项目有些特殊,我需要发布 CPosDeviceSdk.jar 包给其他人使用。jar 在编译过程中会生成,但是它不属于 Android Library 的标准输出。在这种情况下,我需要在编译完成后,主动 copy jar 包到我自己设计的产出物目录中。
复制代码
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Android 自己定义了好多 ScriptBlock。Android 定义的 DSL 参考文档在
https://developer.android.com/tools/building/plugin-for-gradle.html 下载。注意,它居然没有提供在线文档。
图 36 所示为 Android 的 DSL 参考信息。
图 37 为buildToolsVersion和compileSdkVersion的说明:
从图 37 可知,这两个变量是必须要设置的…
5. CPosDeviceServerApk build.gradle
再来看一个 APK 的 build,它包含 NDK 的编译,并且还要签名。根据项目的需求,我们只能签 debug 版的,而 release 版的签名得发布 unsigned 包给领导签名。另外,CPosDeviceServerAPK 依赖 CPosDeviceSdk。
虽然我可以先编译 CPosDeviceSdk,得到对应的 jar 包,然后设置 CPosDeviceServerApk 直接依赖这个 jar 包就好。但是我更希望 CPosDeviceServerApk 能直接依赖于 CPosDeviceSdk 这个工程。这样,整个 posdevice 可以做到这几个 Project 的依赖关系是最新的。
[build.gradle]
复制代码
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
6. 结果展示
在 posdevice 下执行 gradle assemble 命令,最终的输出文件都会拷贝到我指定的目录,结果如图 38 所示:
图 38 所示为 posdevice gradle assemble的执行结果:
4.4.4 实例 2
下面这个实例也是来自一个实际的 APP。这个 APP 对应的是一个单独的 Project。但是根据我前面的建议,我会把它改造成支持 Multi-Projects Build 的样子。即在工程目录下放一个 settings.build。
另外,这个 app 有一个特点。它有三个版本,分别是 debug、release 和 demo。这三个版本对应的代码都完全一样,但是在运行的时候需要从 assets/runtime_config 文件中读取参数。参数不同,则运行的时候会跳转到 debug、release 或者 demo 的逻辑上。
注意:我知道 assets/runtime_config 这种做法不 decent,但,这是一个既有项目,我们只能做小范围的适配,而不是伤筋动骨改用更好的方法。另外,从未来的需求来看,暂时也没有大改的必要。
引入 gradle 后,我们该如何处理呢?
解决方法是:在编译 build、release 和 demo 版本前,在 build.gradle 中自动设置 runtime_config 的内容。代码如下所示:
[build.gradle]
复制代码
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
最终的结果如图 39 所示:
几个问题,为什么我知道有 preXXXBuild 这样的任务?
答案:gradle tasks --all 查看所有任务。然后,多尝试几次,直到成功
五、总结
到此,我个人觉得 Gradle 相关的内容都讲完了。很难相信我仅花了 1 个小时不到的时间就为实例 2 添加了 gradle 编译支持。在一周以前,我还觉得这是个心病。回想学习 gradle 的一个月时间里,走过不少弯路,求解问题的思路也和最开始不一样:
复制代码
|
|
|
|
|
书中说,如果代码没有加 <<,则这个任务在脚本initialization(也就是你无论执行什么任务,这个任务都会被执行,I am myTask都会被输出)的时候执行,如果加了<<,则在 gradle myTask 后才执行。
我开始完全不知道为什么,死记硬背。现在你明白了吗????
这和我们调用 task 这个函数的方式有关!如果没有 <<,则闭包在 task 函数返回前会执行,而如果加了 <<,则变成调用 myTask.doLast 添加一个 Action 了,自然它会等到 grdle myTask 的时候才会执行!
现在想起这个事情我还是很愤怒,API 都说很清楚了…而且,如果你把 Gradle 当做编程框架来看,对于我们这些程序员来说,写这几百行代码,那还算是事嘛??