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

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

  • 理解 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

第七章 创建任务和插件:任务相关_第1张图片
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()
第七章 创建任务和插件:任务相关_第2张图片
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'
}

看下图:


第七章 创建任务和插件:任务相关_第3张图片
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,得到下面结果:

第七章 创建任务和插件:任务相关_第4张图片
image

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

image

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

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

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

第七章 创建任务和插件:任务相关_第5张图片
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')
    }
}
第七章 创建任务和插件:任务相关_第6张图片
image

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

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

//指定,一定是先执行了 hello6 后才可以执行 hello2
hello2.mustRunAfter(hello6)
第七章 创建任务和插件:任务相关_第7张图片
image

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

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

//指定,hello2 依赖 hello6,默认情况下执行 hello2 会先执行 hello6
hello2.dependsOn(hello6)
第七章 创建任务和插件:任务相关_第8张图片
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')
    }
}

你可能感兴趣的:(第七章 创建任务和插件:任务相关)