Groovy :是一种动态语言。
1:这种语言比较有特点,它和 Java 一样,也运行于 Java 虚拟机中。简单粗暴点儿看,你可以认为 Groovy 扩展了 Java 语言,二者关系如下图
。这点和Kotlin很像都是将源文件,先转成jvm能识别的字节码文件。
2:除了语言和 Java 相通外,Groovy 有时候又像一种脚本语言。当执行 Groovy 脚本时,Groovy 会先将其编译成 Java 类字节码,然后通过 Jvm 来执行这个 Java 类。
一:Groovy 语言学习
1:Groovy语言特性
1.1:Groovy 注释标记和 Java 一样,支持//或者/**/
1.2:Groovy 语句可以不用分号结尾。
每行代码不用加分号外,Groovy 中函数调用的时候还可以不加括号,如
println("test") ---> println "test"
1.3 属性相关:
Groovy 中支持动态类型,即定义变量的时候可以不指定其类型。Groovy 中,变量定义可以使用关键字 def。注意,虽然 def 不是必须的,但是为了代码清晰,建议还是使用 def 关键字
def variable1 = 1 //可以不使用分号结尾
def varable2 = "I am a person"
def int x = 1 //变量定义时,也可以直接指定类型
1.4函数相关:
1.4.1 函数定义时,参数的类型也可以不指定。比如
String testFunction(arg1,arg2){//无需指定参数类型...}
1.4.2:除了变量定义可以不指定类型外,Groovy 中函数的返回值也可以是无类型的。
可以不使用 return xxx 来设置 xxx 为函数返回值。如果不使用 return 语句的话,则函数里最后一句代码的执行结果被设置成返回值。比如:
//无类型的函数定义,必须使用 def 关键字
def getSomething(){
"getSomething return value" //如果这是最后一行代码,则返回类型为 String
1000 //如果这是最后一行代码,则返回类型为 Integer
}
//如果指定了函数返回类型,则可不必加 def 关键字来定义函数
String getString(){return "I am a string"}
1.5 Groovy 中的数据类型
除了java定义的数据类型外,groovy有2种比较特殊的数据类型
1.5.1Groovy 中的容器类。(其实只是对java容器进行了拓展,相对比较好理解)
Groovy 中的容器类很简单,就三种:
List:链表,其底层对应 Java 中的 List 接口,一般用 ArrayList 作为真正的实现类。
Map:键-值表,其底层对应 Java 中的 LinkedHashMap。
Range:范围,它其实是 List 的一种拓展。
1.List 类
变量定义:List 变量由[]定义,比如
def aList = [5,'string',true] //List 由[]定义,其元素可以是任何对象
变量存取:可以直接通过索引存取,而且不用担心索引越界。如果索引超过当前链表长度,List 会自动 往该索引添加元素
assert aList[1] == 'string'
assert aList[5] == null //第 6 个元素为空
aList[100] = 100 //设置第 101 个元素的值为 10
assert aList[100] == 100
那么,aList 到现在为止有多少个元素呢?
println aList.size ===>结果是 101
2.Map 类
容器变量定义
变量定义:Map 变量由[:]定义,比如
def aMap = ['key1':'value1','key2':true]
Map 由[:]定义,注意其中的冒号。冒号左边是 key,右边是 Value。key 必须是字符串,value 可以是任何对象。另外,key 可以用''或""包起来,也可以不用引号包起来。比如
def aNewMap = [key1:"value",key2:true] //其中的 key1 和 key2 默认被
处理成字符串"key1"和"key2" 不过 Key 要是不使用引号包起来的话,也会带来一定混淆,比如
def key1="wowo"
def aConfusedMap=[key1:"who am i?"]
aConfuseMap 中的 key1 到底是"key1"还是变量 key1 的值“wowo”?显然,答案是字符串"key1"。如果要是"wowo"的话,则 aConfusedMap 的定义必须设置成:
def aConfusedMap=[(key1):"who am i?"]
Map 中元素的存取更加方便,它支持多种方法:
println aMap.keyName <==这种表达方法好像 key 就是 aMap 的一个成员变量一样
println aMap['keyName'] <==这种表达方法更传统一点
aMap.anotherkey = "i am map" <==为 map 添加新元素
3.Range 类
Range 是 Groovy 对 List 的一种拓展,变量定义和大体的使用方法如下:
def aRange = 1..5 <==Range 类型的变量 由 begin 值+两个点+end 值表示
左边这个 aRange 包含 1,2,3,4,5 这 5 个值
如果不想包含最后一个元素,则
def aRangeWithoutEnd = 1..<5 <==包含 1,2,3,4 这 4 个元素
println aRange.from
println aRange.to
先定位到 Range 类。它位于 groovy.lang 包中:
有了 API 文档,你就可以放心调用其中的函数了。不过,不过,不过:我们刚才代码中用到了 Range.from/to 属性值,但翻看 Range API 文档的时候,其实并没有这两个成员变量。下图 是 Range 的方法
文档中并没有说明 Range 有 from 和 to 这两个属性,但是却有 getFrom 和 getTo 这两个函数。原来:
根据 Groovy 的原则,如果一个类中有名为 xxyyzz 这样的属性(其实就是成员变量),Groovy 会自动为它添加 getXxyyzz 和 setXxyyzz 两个函数,用于获取和设置 xxyyzz 属性值。
注意,get 和 set 后第一个字母是大写的
所以,当你看到 Range 中有 getFrom 和 getTo 这两个函数时候,就得知道潜规则下,Range 有 from 和 to 这两个属性。当然,由于它们不可以被外界设置,所以没有公开 setFrom 和 setTo 函数。
1.5.2 闭包(英文叫 Closure,是 Groovy 中非常重要的一个数据类型或者说一种概念了,脚本中最经常使用)
闭包,是一种数据类型,它代表了一段可执行的代码。其外形如下:
def aClosure = {//闭包是一段代码,所以需要用花括号括起来..
String param1, int param2 -> //这个箭头很关键。箭头前面是参数定义,箭头后面是代码
println "this is code" //这是代码,最后一句是返回值,
//也可以使用 return,和 Groovy 中普通函数一样
}
简而言之,Closure 的定义格式是:
def xxx = {paramters -> code} //或者
def xxx = {无参数,纯 code} 这种 case 不需要->符号
闭包中注意点a:
如果闭包没定义参数的话,则隐含有一个参数,这个参数名字叫 it,和 this 的作用类似。it 代表闭包的参数。如项目中用到的
再如:
def greeting = { "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'
等同于:
def greeting = { it -> "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'
但是,如果在闭包定义时,采用下面这种写法,则表示闭包没有参数!
def noParamClosure = { -> true }
这个时候,我们就不能给 noParamClosure 传参数了!
noParamClosure ("test") <==报错
注意点b
闭包在 Groovy 中大量使用,比如很多类都定义了一些函数,这些函数最后一个参数都是一个闭包。比如:
public static List each(List self, Closure closure)
上面这个函数表示针对 List 的每一个元素都会调用 closure 做一些处理。这里的 closure,就有点回调函数的感觉。但是,在使用这个 each 函数的时候,我们传递一个怎样的 Closure 进去?比如:
def iamList = [1,2,3,4,5] //定义一个 List
iamList.each{ //调用它的 each,这段代码的格式看不懂了吧?each 是个函数,圆括号去哪了?
println it
}
特点经常出现(这就是1.2中指出的函数省略括号特性),因为以后在 Gradle 中经常会出现图 7 这样的代码:
经常碰见图 7 这样的没有圆括号的代码。省略圆括号虽然使得代码简洁,看起来更像脚本语言,以 doLast 为例,完整的代码应该按下面这种写法:
doLast({
println 'Hello world!'
})
有了圆括号,就知道 doLast 只是把一个 Closure 对象传了进去。很明显,它不代表这段脚本解析到 doLast 的时候就会调用 println 'Hello world!' 。但是把圆括号去掉后,就感觉好像 println 'Hello world!'立即就会被调用一样!
注意点c 如何确定 Closure 的参数
能写成下面这样吗?
iamList.each{String name,int x ->
return x
} //运行的时候肯定报错!
所以,Closure 虽然很方便,但是它一定会和使用它的上下文有极强的关联。要不,作为类似回调这样的东西,我如何知道调用者传递什么参数给 Closure 呢?
此问题如何破解?只能通过查询 API 文档才能了解上下文语义。比如下图 :
图 中:
each 函数说明中,将给指定的 closure 传递 Set 中的每一个 item。所以,closure 的参数只有一个。
findAll 中,绝对抓瞎了。一个是没说明往 Closure 里传什么。另外没说明 Closure 的返回值是什么.....。
对 Map 的 findAll 而言,Closure 可以有两个参数。findAll 会将 Key 和 Value 分别传进去。并且,Closure 返回 true,表示该元素是自己想要的。返回 false 表示该元素不是自己要找的。示意代码如图 所示:
Closure 的使用有点坑,很大程度上依赖于你对 API 的熟悉程度,所以最初阶段,SDK 查询是少不了的
1.5.3 String 特点
Groovy 对字符串支持相当强大,充分吸收了一些脚本语言的优点:
1 单引号''中的内容严格对应 Java 中的 String,不对$符号进行转义
def singleQuote='I am $ dolloar' //输出就是 I am $ dolloar
2 双引号""的内容则和脚本语言的处理有点像,如果字符中有$号的话,则它会$表达式先求值。
def doubleQuoteWithoutDollar = "I am one dollar" //输出 I am one dollar
def x = 1
def doubleQuoteWithDollar = "I am $x dolloar" //输出 I am 1 dolloar
3 三个引号'''xxx'''中的字符串支持随意换行 比如
def multieLines = ''' begin
line 1
line 2
end '''
NOTE:这就是为什么gradle脚本中如果 我们想用$占位符来输入版本时,必须用双引号testCompile "com.meiyou:peroid.base:${PERIOD_BASE_VERSION}" ,而如果强制指定版本,单引号和双引号都可以
1.6 Groovy脚本
Groovy 中可以像 Java 那样写 package,然后写类。比如在文件夹 com/cmbc/groovy/目录中放一个文件,叫 Test.groovy,如图 所示:
如果不声明 public/private 等访问权限的话,Groovy 中类及其变量默认都是 public 的,这点与java有所不同。
Java 中,我们最熟悉的是如上图。但是我们在 Java 的一个源码文件中,不能不写 class(interface 或者其他....),而 Groovy 可以像写脚本一样,把要做的事情都写在 xxx.groovy 中,而且可以通过 groovy xxx.groovy 直接执行这个脚本,如
test.groovy 的运行结果是:
println 'Groovy world!'
Groovy 把它转换成这样的 Java 类,下面将进行验证
执行 groovyc -d classes test.groovy
groovyc 是 groovy 的编译命令,-d classes 用于将编译得到的 class 文件拷贝到 classes 文件夹下
是 test.groovy 脚本转换得到的 java class。用 jd-gui 反编译它的代码:
2:Groovy常用API介绍(Groovy 的 API 文档位于http://www.groovy-lang.org/api.html)
2.1文件 I/O 操作
从 Groovy 的文件 I/O 操作掌握下Groovy常用api。比 Java 看起来简单,但要理解起来其实比较难。尤其是当你要自己查 SDK 并编写代码的时候。
整体说来,Groovy 的 I/O 操作是在原有 Java I/O 操作上进行了更为简单方便的封装,并且使用 Closure 来简化代码编写。主要封装了如下一些了类:
2.1.1.读文件
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...
targetFile.eachLine{
String oneLine ->
println oneLine
}
2 直接得到文件内容
targetFile.getBytes() <==文件内容一次性读出,返回类型为 byte[] 注意前面提到的 getter 和 setter 函数,这里可以直接使用 targetFile.bytes //....
3 使用 InputStream.InputStream 的 SDK 在http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/InputStream.html
def ism = targetFile.newInputStream()
//操作 ism,最后记得关掉
ism.close
4 使用闭包操作 inputStream,以后在 Gradle 里会常看到这种搞法
targetFile.withInputStream{ ism ->
操作 ism. 不用 close。Groovy 会自动替你 close
}
确实够简单,令人发指。我当年死活也没找到 withInputStream 是个啥意思。所以,请各位开发者牢记 Groovy I/O 操作相关类的 SDK 地址:
java.io.File:http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/File.htmljava.io.InputStream:http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/InputStream.html
java.io.OutputStream:http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/OutputStream.htmljava.io.Reader:http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/Reader.htmljava.io.Writer:http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/Writer.htmljava.nio.file.Path:http://docs.groovy-lang.org/latest/html/groovy-jdk/java/nio/file/Path.html
2.2.2.写文件
和读文件差不多。不再啰嗦。这里给个例子,如何 copy 文件。
def srcFile = new File(源文件名)
def targetFile = new File(目标文件名)
targetFile.withOutputStream{ os->
srcFile.withInputStream{ ins->
os << ins //利用 OutputStream 的<<操作符重载,完成从 inputstream 到 OutputStream
//的输出
}
}
关于 OutputStream 的<<操作符重载,查看 SDK 文档后可知:
...
NOTE:Groovy及gradle中的 << 并不是类似 C++中的插入运算符,而是相应对象的leftShift函数。如项目中
其实是做了两件事(将在gradle部分析)
1:调用project的task函数为project增加一个task
2:调用 task对象的leftShift函数,传入一个clouse作为参数,而task的leftShift函数其实和dolast是一个作用
所以不同对象的不同<<符号是不一样的
XML 操作
除了 I/O 异常简单之外,Groovy 中的 XML 操作也极致得很。Groovy 中,XML 的解析提供了和 XPath 类似的方法,名为 GPath。这是一个类,提供相应 API。关于 XPath,请脑补https://en.wikipedia.org/wiki/XPath。
GPath 功能包括:给个例子好了,来自 Groovy 官方文档。
test.xml 文件:
Don Xijote
Manuel De Cervantes
Catcher in the Rye
JD Salinger
Alice in Wonderland
Lewis Carroll
Don Xijote
Manuel De Cervantes
现在来看怎么玩转 GPath:
//第一步,创建 XmlSlurper 类
def xparser = new XmlSlurper()
def targetFile = new File("test.xml")
//轰轰的 GPath 出场
GPathResult gpathResult = xparser.parse(targetFile)
//开始玩 test.xml。现在我要访问 id=4 的 book 元素。
//下面这种搞法,gpathResult 代表根元素 response。通过 e1.e2.e3 这种
//格式就能访问到各级子元素....
def book4 = gpathResult.value.books.book[3]
//得到 book4 的 author 元素
def author = book4.author
//再来获取元素的属性和 textvalue
assert author.text() == ' Manuel De Cervantes '
获取属性更直观
author.@id == '4' 或者 author['@id'] == '4'
属性一般是字符串,可通过 toInteger 转换成整数
author.@id.toInteger() == 4
使用 Gradle 的时候有个需求,就是获取 AndroidManifest.xml 版本号(versionName)。有了 GPath,一行代码搞定,请看:
def androidManifest = new XmlSlurper().parse("AndroidManifest.xml")
println androidManifest['@android:versionName']
或者
println androidManifest.@'android:versionName'
groovy既然是一门编程语言,就能做很多事,本文只是列举常用及学习gradle中部分必须了解的知识
二:gradle相关学习
Gladle 可以理解成基G于Groovy的一套框架,同时也是DSL。
1:框架中常用概念介绍
https://docs.gradle.org/current/dsl/
https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html
这2个文档非常重要重点,介绍了gradle框架学习中必须掌握的DS。
Gradle 主要有三种对象,这三种对象和三种不同的脚本文件对应,在 gradle 执行的时候,会将脚本转换成对应的对端:
Gradle 对象:当我们执行 gradle xxx 或者什么的时候,gradle 会从默认的配置脚本中构造出一个 Gradle 对象。在整个执行过程中,只有这么一个对象。Gradle 对象的数据类型就是 Gradle。我们一般很少去定制这个默认的配置脚本。
Project 对象:每一个 build.gradle 会转换成一个 Project 对象。
Settings 对象:显然,每一个 settings.gradle 都会转换成一个 Settings 对象。
脚本中除了可以用到的delegate object外还有Scriptinterface 的api,因为:
而具体一个脚本中能有多少方法及多少属性可用,主要来源于以下几个方面
每一个对象都有对应的API,具体可以点开 api文档进行查阅。
2:gradle命令介绍
点开AS gradle窗口即可看到目前项目中能执行的task(当然也可执行tasks任务查看。注:不包含引入的插件所 定义的可执行task,如需查看所有需加上all参数执行),执行gradle +taskName或者右键点击对应task即可执行对应task ,如执行projects,即可看到项目相应工程信息
3:gradle工作流程
Gradle 工作大概包含三个阶段:
首先是初始化阶段。对我们前面的 multi-project build 而言,就是执行 settings.gradle
Initiliazation phase 的下一个阶段是 Configration 阶段。
Configration 阶段的目标是解析每个 project 中的 build.gradle。比如 multi-project build 例子中,解析每个子目录中的 build.gradle。在这两个阶段之间,我们可以加一些定制化的 Hook。这当然是通过 API 来添加的。
Configuration 阶段完了后,整个 build 的 project 以及内部的 Task 关系就确定了。一个 Project 包含很多 Task,每个 Task 之间有依赖关系。Configuration 会建立一个有向图来描述 Task 之间的依赖关系。所以,我们可以添加一个 HOOK,即当 Task 关系图建立好后,执行一些操作。
4:gradle常用api介绍(官方文档位置 https://docs.gradle.org/current/javadoc/)
app 有一个特点。它有三个版本,分别是 debug、release 和 demo。这三个版本对应的代码都完全一样,但是在运行的时候需要从 assets/runtime_config 文件中读取参数。参数不同,则运行的时候会跳转到 debug、release 或者 demo 的逻辑上。
引入 gradle 后,我们该如何处理呢?
解决方法是:在编译 build、release 和 demo 版本前,在 build.gradle 中自动设置 runtime_config 的内容。代码如下所示:
[build.gradle]
apply plugin: 'com.android.application' //加载 APP 插件
//加载 utils.gradle
apply from: rootProject.getRootDir().getAbsolutePath() + "/utils.gradle"
//buildscript 设置 android app 插件的位置
buildscript {
repositories { jcenter() }
dependencies { classpath 'com.android.tools.build:gradle:1.2.3' }
}
//android ScriptBlock
android {
compileSdkVersion gradle.api
buildToolsVersion "22.0.1"
sourceSets{ //源码设置 SB
main{
manifest.srcFile 'AndroidManifest.xml'
jni.srcDirs = []
jniLibs.srcDir 'libs'
aidl.srcDirs=['src']
java.srcDirs=['src']
res.srcDirs=['res']
assets.srcDirs = ['assets'] //多了一个 assets 目录
}
}
signingConfigs {//签名设置
debug { //debug 对应的 SB。注意
if(project.gradle.debugKeystore != null){
storeFile file("file://${project.gradle.debugKeystore}")
storePassword "android"
keyAlias "androiddebugkey"
keyPassword "android"
}
}
}
/*
最关键的内容来了: buildTypes ScriptBlock.
buildTypes 和上面的 signingConfigs,当我们在 build.gradle 中通过{}配置它的时候,
其背后的所代表的对象是 NamedDomainObjectContainer 和
NamedDomainObjectContainer
注意,NamedDomainObjectContainer是一种容器,
容器的元素是 BuildType 或者 SigningConfig。我们在 debug{}要填充 BuildType 或者
SigningConfig 所包的元素,比如 storePassword 就是 SigningConfig 类的成员。而 proguardFile 等
是 BuildType 的成员。
那么,为什么要使用 NamedDomainObjectContainer 这种数据结构呢?因为往这种容器里
添加元素可以采用这样的方法: 比如 signingConfig 为例
signingConfig{//这是一个 NamedDomainObjectContainer
test1{//新建一个名为 test1 的 SigningConfig 元素,然后添加到容器里
//在这个花括号中设置 SigningConfig 的成员变量的值
}
test2{//新建一个名为 test2 的 SigningConfig 元素,然后添加到容器里
//在这个花括号中设置 SigningConfig 的成员变量的值
}
}
在 buildTypes 中,Android 默认为这几个 NamedDomainObjectContainer 添加了
debug 和 release 对应的对象。如果我们再添加别的名字的东西,那么 gradle assemble 的时候
也会编译这个名字的 apk 出来。比如,我添加一个名为 test 的 buildTypes,那么 gradle assemble
就会编译一个 xxx-test-yy.apk。在此,test 就好像 debug、release 一样。
*/
buildTypes{
debug{ //修改 debug 的 signingConfig 为 signingConfig.debug 配置
signingConfig signingConfigs.debug
}
demo{ //demo 版需要混淆
proguardFile 'proguard-project.txt'
signingConfig signingConfigs.debug
}
//release 版没有设置,所以默认没有签名,没有混淆
}
......//其他和 posdevice 类似的处理。来看如何动态生成 runtime_config 文件
def runtime_config_file = 'assets/runtime_config'
/*
我们在 gradle 解析完整个任务之后,找到对应的 Task,然后在里边添加一个 doFirst Action
这样能确保编译开始的时候,我们就把 runtime_config 文件准备好了。
注意,必须在 afterEvaluate 里边才能做,否则 gradle 没有建立完任务有向图,你是找不到
什么 preDebugBuild 之类的任务的
*/
project.afterEvaluate{
//找到 preDebugBuild 任务,然后添加一个 Action
tasks.getByName("preDebugBuild"){
it.doFirst{
println "generate debug configuration for ${project.name}"
def configFile = new File(runtime_config_file)
configFile.withOutputStream{os->
os << I am Debug\n' //往配置文件里写 I am Debug
}
}
}
//找到 preReleaseBuild 任务
tasks.getByName("preReleaseBuild"){
it.doFirst{
println "generate release configuration for ${project.name}"
def configFile = new File(runtime_config_file)
configFile.withOutputStream{os->
os << I am release\n'
}
}
}
//找到 preDemoBuild。这个任务明显是因为我们在 buildType 里添加了一个 demo 的元素
//所以 Android APP 插件自动为我们生成的
tasks.getByName("preDemoBuild"){
it.doFirst{
println "generate offlinedemo configuration for ${project.name}"
def configFile = new File(runtime_config_file)
configFile.withOutputStream{os->
os << I am Demo\n'
}
}
}
}
}
.....//copyOutput