第七章 创建任务和插件:任务相关

第七章 创建任务和插件:任务相关

  • 理解 Groovy
  • 任务入门
  • Hooking Android 插件
  • 编写自定义插件

一、理解 Groovy

1. 简介

Groovy 是从 Java 衍生出来的,运行在 Java 虚拟机上的敏捷语言。
其目标是,不管是作为脚本语言还是编程语言,都可简单、直接使用。
首先我们来看一下 GroovyJava 两种语言之间的差异:

  • 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

image

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()
image
  • 使用关键字 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)

输出结果为:
image
  • 在 java 中,方法是不能直接被输出的,方法名() 是调用方法,没有直接使用方法名的操作
  • 此处和 js 相似,如果方法引用后面加了() ,说明是要执行方法,如果没有加()那么就不会执行方法体中的内容,一般是一种数据的传递。

5. 集合

Gradle 中使用 Groovy 时,有两个重要的集合类型:listsmaps
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'
}

看下图:


image

也就是说 dependencies 方法接受的参数类型是一个 Closure 的类型,也就是一个代码块。
在 KT 中也是一样的,若方法 a 的参数列表只有一个参数且是闭包,那么在使用 a 方法的时候:a{} 即可,{} 就是传递到 a 方法的参数。

将 dependencies() 方法传递给 Project 对象。该 closure 被传递给一个包含 add() 方法的 DependencyHandler。add() 方法接受三个参数:一个定义配置的字符串,一个定义依赖标志的对象,一个针对依赖配置特定属性的 closure,在官方文档中找到下图:

image

如果将依赖项完整写出时:

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。其实这句代码就已经创建了任务,但当你执行时,它不会做任何时,因为该任务没有动作,要创建一个有用的任务,我们给任务添加动作!

image

//在 app 的 build.gradle 文件中添加下面代码,定义一个task
task hello {
    println 'hello, my task!'
}

在命令行中执行:gradlew hello,得到下面结果:

image

正常情况下,我们以为是 gradlew hello 命令执行了该task,实际上不是这样的,我们可以尝试在命令行中输入 :gradlew help 会发现 hello 任务的输出也出现了。

image

在任一 Gradle 构建中,都有三个阶段:初始化阶段、配置阶段、执行阶段。
当像上个例子那样以相同方式添加代码到一个任务时,我们实际上是设置了任务的配置。所以即使执行了不同的任务,'hello, my task!' 依然会被输出。
如果想在执行阶段给一个任务添加动作,需要使用下面的表示法:

// << 是在告知 Gradle  hello2任务要在执行阶段执行,而不是在配置阶段执行
task hello2 << {
    println('我是第二个')
}

我们在命令行中通过:gradlew hello2 执行 hello2 任务,输出结果如下:

image

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')
    }
}
image

doFirst() 是类似于栈的处理方式(先进后出),也就是说最后添加的一定是第一个执行的。
doLast() 是类似于队列的处理方式(先进先出),也就是说最后添加的一定是最后执行的。

当涉及到给 tasks 排序时,我们可以使用 mustRunAfter() 方法。该方法将影响 Gradle 如何构建依赖关系。当使用 mustRunAfter() 时,我们需要指定,如果两个任务都被执行,那么必须有一个任务始终先执行:

//指定,一定是先执行了 hello6 后才可以执行 hello2
hello2.mustRunAfter(hello6)
image

mustRunAfter() 只是用作排序,hello2 和 hello6 都可以分别单独执行。

使用 dependsOn() 可以使一个任务依赖另一个任务:

//指定,hello2 依赖 hello6,默认情况下执行 hello2 会先执行 hello6
hello2.dependsOn(hello6)
image

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')
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容