第七章 创建任务和插件:任务相关
- 理解
Groovy
- 任务入门
- Hooking Android 插件
- 编写自定义插件
一、理解 Groovy
1. 简介
Groovy
是从 Java 衍生出来的,运行在 Java 虚拟机上的敏捷语言。
其目标是,不管是作为脚本语言还是编程语言,都可简单、直接使用。
首先我们来看一下 Groovy
和 Java
两种语言之间的差异:
- 在 Java 中,打印字符串:
System.out.println("Hello, world!")
- 在 Groovy 中:
println 'Hello, world!'
- 不同之处:
没有 System.out
方法周围没有括号
一行末尾没有分号
例子中,在字符串周围使用了 单引号。对于字符串,Groovy
中既可以使用 单引号 也可以使用 双引号,不同之处在于,双引号字符串 可以插入表达式。
插值
是评估包含 占位符 的字符串,并通过它们的值 替换
占位符的过程。这些占位符表达式可以是变量或方法。包含一个方法或多个变量的占位符表达式,需要有 $
前缀并被花括号包裹(K.....KT?),包含一个单独变量的占位符表达式可以只含有 $
前缀(就....就是KT)。一些示例:
def name = 'Andy'
def greeting = "Hello, $name!"
def name_size "Your name is ${name.size()} characters long."
字符串插值还运行动态执行代码:
def method = 'toString'
//也就是可以在运行期通过改变字符串的方式改变要执行的方法
//这个 KT 没有
//这是动态编程语言特有的方式
new Date()."$method"()
2. 类和成员变量
可以使用 IDEA 直接创建 Groovy 工程,去 Groovy 官网下载 Groovy SDK
在 Groovy
中创建一个类和在 Java
中创建一个类相似。下面是一个仅包含一个成员变量的类:
class HelloGroovy {
String greeting
String getGreeting(){
return 'hello'
}
}
无论是类,还是成员变量,都没有明确的访问修饰符。Groovy
中的默认访问修饰符与 Java
不同。
- 类和方法:默认公有
- 类成员:默认私有
Groovy
不需要 main 方法作为入口,直接将下面代码写在文件中就可以:(右键直接 run)
def instance = new HelloGroovy()
instance.setGreeting('hello, Groovy')
println instance.getGreeting()
- 使用关键字
def
来创建新的变量。 - 每个变量会默认存在(隐式)setter/getter
3. 方法
Java 方法与 Groovy 方法的示例对比:
//方法的定义
public int square(int num){
return num * num;
}
//方法的调用
square(2)
- 需要制定权限修饰符,返回值类型,参数列表
//方法的定义
def square(def num){
num * num
}
//方法的调用
square 4
- 返回类型和参数类型都没有明确的定义,与定义变量时一样使用
def
关键字 - 没有使用
return
的情况下,方法隐晦的返回了末尾的表达式结果
(建议还是使用 return 可读性较高) - 调用该方法时,不需要 括号 或 分号
还有另外一种利用 Groovy
来定义新方法的剪短方式:
//方法的定义
def square = { num ->
num * num
}
//方法的调用
square 8
这是一个 closure
(闭包
)
4. Closures
Closures 是匿名代码块,可以接受参数和返回值。它们可以被视为变量,被当做参数传递给方法。
按照一个 java 程序员的角度来看待的话:
- def 是在栈内存申请一块地址(引用)
- 这个引用要指向谁,在 java 强类型语言中,是要表明目标类型的
- 而在 Groovy 可以不用指明目标类型,直接使用 def 表示即可(kotlin 使用 var)
- def 不止可以指向一个对象,还可以指向一个方法体 (对象和方法体都被放在堆里(我没记错吧。。。))
- 所以在 Groovy 中,可以把方法体看成一个对象来使用
- 参数可以是一个对象,那么就可以是一个方法体
- 返回值可以是一个对象,那么就可以是一个方法体
可以使用 Closure
明确的表示声明一个 closure
,这样比使用 def 有更高的可读性:
Closure square = {
it * it
}
//idea 在提示时会默认提供()
square(16)
如果我们没有给 closure
指定一个参数,则 Groovy
会自动添加一个,这个参数的参数名通常是 it
,我们可以在所有的 closure
中使用它。
如果调用者没有传入任何参数,则在 closure
中的 it 为空。
注: 前提条件是 closure
只有一个参数
定义一个类 HelloClosure
,在该类中定义一个closure c
,在 Main 类中将 c 输出:
class HelloClosure {
Closure c = {
it * it + it * 3
}
}
//main 中输出
def helloClosure = new HelloClosure()
println(helloClosure.c)
输出结果为:
- 在 java 中,方法是不能直接被输出的,方法名() 是调用方法,没有直接使用方法名的操作
- 此处和 js 相似,如果方法引用后面加了() ,说明是要执行方法,如果没有加()那么就不会执行方法体中的内容,一般是一种数据的传递。
5. 集合
在 Gradle 中使用 Groovy 时,有两个重要的集合类型:lists
和 maps
。
在 Groovy 中创建一个新的 list
非常容易,无须初始化,如下所示创建一个简单的list:
class HelloList {
//定义
List list = [1, 2, 3, 4, 5]
//方法
def show = {
//迭代list
list.each { element ->
//使用手动定义的参数
println(element)
}
list.each {
//使用默认的参数
println("默认的参数:" + it)
}
}
}
map:
class HelloMap {
//可以给定具体的类型(Map)
//也可以直接使用def来定义
def map = [width: 10, height : 20]
def show = {
println("map.width:"+map.width)
//这三种方式都是一样的
println("map.height:"+map.get('height'))
println("map.height:"+map.height)
println("map.height:"+map['width'])
}
}
6. Gradle 中的 Groovy
我们来观察一下 Gradle 的构建文件,下面这行代码:
apply plugin:'com.anroid.application'
这段代码完全是 Groovy
的简写,如果没有简写的情况下,实际是这样的:project.apply([plugin: 'com.anroid.application'])
apply()
是 Project 类的一个方法,Project 类是每个 Gradle 构建的基础构建代码块。apply()
需要一个参数,是一个Map,里面含有一个 key 为 plugin,value 为 com.anroid.application 的 Entry。
看下面这个 dependencies
代码块:
dependencies{
implementation 'com.android.support:appcompat-v7:28.0.0'
}
看下图:
也就是说 dependencies 方法接受的参数类型是一个 Closure
的类型,也就是一个代码块。
在 KT 中也是一样的,若方法 a 的参数列表只有一个参数且是闭包,那么在使用 a 方法的时候:a{}
即可,{} 就是传递到 a 方法的参数。
将 dependencies() 方法传递给 Project 对象。该 closure 被传递给一个包含 add()
方法的 DependencyHandler
。add() 方法接受三个参数:一个定义配置的字符串,一个定义依赖标志的对象,一个针对依赖配置特定属性的 closure,在官方文档中找到下图:
如果将依赖项完整写出时:
project.dependencies({/dependencies接受的closure
//在这个 closure 中调用了 add方法
add('implementation', 'com.android.support:appcompat-v7:28.0.0',{
//这个是 add 接受到的 closure
//在这个 closure 中,可以指定一些配置信息
//比如 exclude
}
})
二、任务入门
任务可以操作存在的构建进程,添加新的构建步骤,或影响构建输出。
例如:通过 hooking 到 Gradle 的 Android 插件,给一个生产的 APK 重命名。
任务也可以运行更加复杂的代码,可以在应用打包之前生成几张不同密度的图片。
一旦知道如何创建自己的任务,就可以在构建过程的任何细节上进行修改(想想就很刺激)
1. 定义任务
任务属于一个 Project
对象,并且每个任务都可以执行 task
接口。定义一个新任务最简单的方式是,执行将任务名称作为参数的任务方法:task hello
。其实这句代码就已经创建了任务,但当你执行时,它不会做任何时,因为该任务没有动作,要创建一个有用的任务,我们给任务添加动作!
//在 app 的 build.gradle 文件中添加下面代码,定义一个task
task hello {
println 'hello, my task!'
}
在命令行中执行:gradlew hello
,得到下面结果:
正常情况下,我们以为是 gradlew hello
命令执行了该task,实际上不是这样的,我们可以尝试在命令行中输入 :gradlew help
会发现 hello
任务的输出也出现了。
在任一 Gradle 构建中,都有三个阶段:初始化阶段、配置阶段、执行阶段。
当像上个例子那样以相同方式添加代码到一个任务时,我们实际上是设置了任务的配置。所以即使执行了不同的任务,'hello, my task!'
依然会被输出。
如果想在执行阶段给一个任务添加动作,需要使用下面的表示法:
// << 是在告知 Gradle hello2任务要在执行阶段执行,而不是在配置阶段执行
task hello2 << {
println('我是第二个')
}
我们在命令行中通过:gradlew hello2
执行 hello2 任务,输出结果如下:
Groovy 有很多简写,在 Gradle 中定义任务的常用方式有以下几种:
//下面三个实现的事情是一样的
task hello2 {
}
task(hello3) << {
}
task('hello4') << {
}
//tasks 是一个 TaskContainer 实例
//TaskContainer 实例存在于 Project 对象中
tasks.create(name:'hello5') << {
}
2. 任务剖析
Task 接口是所有任务的基础,定义了一系列属性和方法,所有这些都是由一个叫做 DefaultTask
的类实现的。我们没创建的一个新的任务,都是基于 DefaultTask
的。
每个任务都包含一个 Actioin
对象的集合。当一个任务被执行时,所有这些动作都会以连续的顺序被执行。
我们可以使用 doFirst()
doLast()
方法来为一个任务添加动作。这两个方法都是以一个 closure
作为参数,然后被包装到一个 Action
对象中的。
前面的 <<
就是 doFirst()
方法的简写。
有趣的地方来了,编写一个 task 如下:
//这里没有使用 <<
//说明在配置阶段就会执行 hello6
task hello6 {
println('这是 hello6')
//下面的 doFirst 或 doLast 不会配置阶段执行,而是在执行阶段执行
doLast{
println('doLast----1')
}
doFirst{
println('doFirst----1')
}
doFirst {
println('doFirst----2')
}
doLast{
println('doLast----2')
}
}
doFirst()
是类似于栈的处理方式(先进后出),也就是说最后添加的一定是第一个执行的。
doLast()
是类似于队列的处理方式(先进先出),也就是说最后添加的一定是最后执行的。
当涉及到给 tasks 排序时,我们可以使用 mustRunAfter()
方法。该方法将影响 Gradle 如何构建依赖关系。当使用 mustRunAfter()
时,我们需要指定,如果两个任务都被执行,那么必须有一个任务始终先执行:
//指定,一定是先执行了 hello6 后才可以执行 hello2
hello2.mustRunAfter(hello6)
mustRunAfter()
只是用作排序,hello2 和 hello6 都可以分别单独执行。
使用 dependsOn()
可以使一个任务依赖另一个任务:
//指定,hello2 依赖 hello6,默认情况下执行 hello2 会先执行 hello6
hello2.dependsOn(hello6)
3. 使用任务来简化 release 过程
发布一个 Android 应用需要使用证书对其签名,其中包含一对私钥。我们创建好 keystore 后就可以在 Gradle 中按如下方式定义配置:
android {
compileSdkVersion 28
defaultConfig {
applicationId "zyf.com.simplereleasebygradletask"
minSdkVersion 19
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
signingConfigs {
release {
storeFile file('release.keystore')
storePassword '123456'
keyAlias 'ReleaseKey'
keyPassword '111111'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
//声明使用 签名配置中的 release
signingConfig signingConfigs.release
}
}
}
这种方法的缺点是:keystone 密码是以明文的形式存放在依赖仓库中的,肯定不能这么做。
可以创建一个配置文件,用来存放 keystore
密码。
在项目的根目录中创建一个名为 private.properties
的文件,添加如下代码:
release.keystore.password = 123456
release.keypassword = 111111
在 app 下的 build.gradle
中添加如下代码:
tasks{
task getReleasePassword << {
def storePassword = ''
def keyPassword = ''
//如果有这个属性文件
if(rootProject.file('private.properties').exists()){
def properties = new Properties()
//就把这个文件的信息加载到我们建立的 properties 对象中
properties.load(rootProject.file('private.properties').newDataInputStream())
storePassword = properties.getProperty('release.keystore.password')
keyPassword = properties.getProperty('release.keypassword')
}
//这里的 ? 与 KT 中相同
if(!storePassword?.trim()){
storePassword = new String(System.console().readPassword('\n 请输入 StorePassword :'))
}
if(!keyPassword?.trim()){
keyPassword = new String(System.console().readPassword('\n 请输入 KeyPassword :'))
}
//拿到了 两个密码,就可以赋值了
android.signingConfigs.release.storePassword = storePassword
android.signingConfigs.release.keyPassword = keyPassword
}
}
还有个问题,要保证每次 构建 release 版本时,getReleasePassword 任务都必须先执行,在 构建文件中添加如下代码:
//要保证每次 构建 release 版本时,getReleasePassword 任务都必须先执行
//这里为什么不能直接使用 dependsOn ,而是要使用 whenTaskAdded()方法?
//答:Gradle 的 Android 插件是基于构建 variant 动态生成的 packaing 任务,
// 意思就是在 Android 插件发现所有构建variant之前,packageRelease 任务都不会存在
// 即:发现过程是在每个单独构建之前完成的
//
tasks.whenTaskAdded { addedTask ->
//所以当发现过程完成了,就会将 packageRelease 加入到任务中
//此时在对 packageRelease 设置依赖项
if(addedTask.name == 'packageRelease'){
addedTask.dependsOn('getReleasePassword')
}
}