Gradle 系列 (二)、Gradle 技术探索

前言

很高兴遇见你~

这又是一个新的系列,关于 Gradle 学习,我所理解的流程如下图:

Gradle 系列 (二)、Gradle 技术探索

在本系列的上一篇文章中,我们对 Gradle 的一些基础概念及 Groovy 语法进行了讲解,完成了第一个环节。还没有看过上一篇文章的朋友,建议先去阅读 Gradle 系列 (一)、Gradle相关概念理解,Groovy基础。

今天我们主要介绍环节二:熟悉 Gradle 常用 API,了解 Settings,Project,Task 等等。

Github Demo 地址:https://github.com/sweetying520/GradleDemo , 大家可以结合 demo 一起看,效果杠杠滴🍺

下面就正式进入 Gradle 的学习

一、Gradle 构建流程

1)、Gradle 构建阶段

Gradle 构建流程主要分为三个阶段:

1、初始化阶段

2、配置阶段

3、执行阶段

1、初始化阶段

Gradle 初始化阶段主要就是执行 settings.gradle 脚本,构建 Project 对象

我们使用 AndroidStudio 新建一个 Android 项目的时候会自动生成 settings.gradle 文件,内容如下:

rootProject.name = "GradleDemo"
include ':app'

1、指定项目根 Project 的名称

2、使用 include 导入 app 工程

实际上 settings.gradle 对应一个 Settings 对象,include 就是 Settings 对象下的一个方法,它的作用就是引用哪些工程需要加入构建。然后 Gradle 会为每个带有 build.gradle 脚本文件的工程构建一个与之对应的 Project 对象

1、include 扩展

我们可以使用 include + project 方法引用任何位置下的工程,如下:

include ':test'
project(':test').projectDir = file('当前工程的绝对路径')

通常我会使用这种方式引用自己写的库进行调试,非常的方便

但有的时候会遇到同时引入了 AAR 和源码的情况,我们可以使用 include + project,结合一些其他的配置,来实现 AAR 和源码的快速切换,具体步骤如下:

//步骤一:在 settings.gradle 中引入源码工程
include ':test'
project(':test').projectDir = file('当前工程的绝对路径')

//步骤二:在根 build.gradle 下进行如下配置
allprojects {
    configurations.all {
        resolutionStrategy {
            dependencySubstitution {
                substitute module("com.dream:test") with project(':test')
            }
        }
    }
}
2、Settings

关于 Settings 的所有属性和方法,如下图:

Gradle 系列 (二)、Gradle 技术探索

结合官网提供的文档:https://docs.gradle.org/current/Javadoc/org/gradle/api/initialization/Settings.html 去查看,效果杠杠的😄

2、配置阶段

Gradle 配置阶段主要就是解析 Project 对象(build.gradle 脚本文件),构建 Task 有向无环图

配置阶段会执行的代码:除 Task 的 Action 中编写的代码都会被执行,不懂 Action 的继续往下看,后面会讲到。如:

1、build.gradle 中的各种语句

2、Task 配置段语句

配置阶段完成后,整个工程的 Task 依赖关系都确定了,我们可以通过 Gradle  对象的 getTaskGraph 方法访问 Task ,对应的类为 TaskExecutionGraph ,关于 TaskExecutionGraph API 文档:https://docs.gradle.org/current/javadoc/org/gradle/api/execution/TaskExecutionGraph.html

注意: 执行任何 Gradle 命令,在初始化阶段和配置阶段的代码都会被执行

3、执行阶段

Gradle 执行阶段主要就是执行 Task 及其依赖的 Task

2)、Gradle 生命周期 Hook 点

引用 joe_H 一张完整的 Gradle 生命周期图,如下:

Gradle 系列 (二)、Gradle 技术探索

上图对 Gradle 生命周期总结的很到位,我们解析一波:

注意:Gradle 执行脚本文件的时候会生成对应的实例,主要有如下三种对象:

1、Gradle 对象:在项目初始化时构建,全局单例存在,只有这一个对象

2、Project 对象:每一个 build.gradle 都会转换成一个 Project 对象

3、Settings 对象:Seetings.gradle 会转变成一个 Seetings 对象

1、Gradle 在各个阶段都提供了生命周期回调,在添加监听器的时候需要注意:监听器要在生命周期回调之前添加,否则会导致有些回调收不到

2、Gradle 初始化阶段

  • 在 settings.gradle 执行完后,会回调 Gradle 对象的 settingsEvaluated 方法
  • 在构建所有工程 build.gradle 对应的 Project 对象后,也就是初始化阶段完毕,会回调 Gradle 对象的 projectsLoaded 方法

3、Gradle 配置阶段:

  • Gradle 会循环执行每个工程的 build.gradle 脚本文件
  • 在执行当前工程 build.gradle 前,会回调 Gradle 对象的 beforeProject 方法和当前 Project 对象的 beforeEvaluate 方法
  • 在执行当前工程 build.gradle 后,会回调 Gradle 对象的 afterProject 方法和当前 Project 对象的 afterEvaluate 方法
  • 在所有工程的 build.gradle 执行完毕后,会回调 Gradle 对象的 projectsEvaluated 方法
  • 在构建 Task 依赖有向无环图后,也就是配置阶段完毕,会回调 TaskExecutionGraph 对象的 whenReady 方法

注意:Gradle 对象的 beforeProject,afterProject 方法和 Project 对象的 beforeEvaluate ,afterEvaluate 方法回调时机是一致的,区别在于:

1、Gradle 对象的 beforeProject,afterProject 方法针对项目下的所有工程,即每个工程的 build.gradle 执行前后都会收到这两个方法的回调

2、 Project 对象的 beforeEvaluate ,afterEvaluate 方法针对当前工程,即当前工程的 build.gradle 执行前后会收到这两个方法的回调

4、执行阶段:

  • Gradle 会循环执行 Task 及其依赖的 Task
  • 在当前 Task 执行之前,会回调 TaskExecutionGraph 对象的 beforeTask 方法
  • 在当前 Task 执行之后,会回调 TaskExecutionGraph 对象的 afterTask 方法

5、当所有的 Task 执行完毕后,会回调 Gradle 对象的 buildFinish 方法

了解了 Gradle 生命周期后,我们就可以根据自己的需求添加 Hook。例如:我们可以打印 Gradle 构建过程中,各个阶段及各个 Task 的耗时

3)、打印 Gradle 构建各个阶段及各个任务的耗时

在 settings.gradle 添加如下代码:

//初始化阶段开始时间
long beginOfSetting = System.currentTimeMillis()
//配置阶段开始时间
def beginOfConfig
//配置阶段是否开始了,只执行一次
def configHasBegin = false
//存放每个 build.gradle 执行之前的时间
def beginOfProjectConfig = new HashMap()
//执行阶段开始时间
def beginOfTaskExecute
//初始化阶段执行完毕
gradle.projectsLoaded {
    println "初始化总耗时 ${System.currentTimeMillis() - beginOfSetting} ms"
}

//build.gradle 执行前
gradle.beforeProject {Project project ->
    if(!configHasBegin){
        configHasBegin = true
        beginOfConfig = System.currentTimeMillis()
    }
    beginOfProjectConfig.put(project,System.currentTimeMillis())
}

//build.gradle 执行后
gradle.afterProject {Project project ->
    def begin = beginOfProjectConfig.get(project)
    println "配置阶段,$project 耗时:${System.currentTimeMillis() - begin} ms"
}

//配置阶段完毕
gradle.taskGraph.whenReady {
    println "配置阶段总耗时:${System.currentTimeMillis() - beginOfConfig} ms"
    beginOfTaskExecute = System.currentTimeMillis()
}

//执行阶段
gradle.taskGraph.beforeTask {Task task ->
    task.doFirst {
        task.ext.beginOfTask = System.currentTimeMillis()
    }

    task.doLast {
        println "执行阶段,$task 耗时:${System.currentTimeMillis() - task.ext.beginOfTask} ms"
    }
}

//执行阶段完毕
gradle.buildFinished {
    println "执行阶段总耗时:${System.currentTimeMillis() - beginOfTaskExecute}"
}

//执行 Gradle 命令
./gradlew clean

//打印结果如下:
初始化总耗时 140 ms

> Configure project :
配置阶段,root project 'GradleDemo' 耗时:1181 ms

> Configure project :app
配置阶段,project ':app' 耗时:1122 ms
配置阶段总耗时:2735 ms

Task :clean
执行阶段,task ':clean' 耗时:0 ms

Task :app:clean
执行阶段,task ':app:clean' 耗时:1 ms
执行阶段总耗时:325

了解了 Gradle 的三个阶段及生命周期,接下来我们就学习 Gradle 的一些核心 API

二、Project 介绍

对于一个 Android 项目,build.gradle 脚本文件是我们经常操作的文件之一,而每个 build.gradle 就对应了一个 Project 对象,因此学习好   Project 对应的 API 能帮助我们更好的去操作 build.gradle 脚本文件, 同时也能看懂大佬们所写的一些配置语句。

Gradle 系列 (二)、Gradle 技术探索

首先看一眼我的项目结构,后续就是基于它来做演示

注意

1、下面所演示的 API 都是一些常用的 API,对 API 使用有疑问的可以去查询官方文档

2、API 的演示如果没做特殊说明,则是在 app 的 build.gradle 文件下操作的

1)、Project API

Project API 文档:https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html ,我们主要介绍一些常用的 API

1、getRootProject 方法

获取根 Project 对象

println getRootProject()

//打印结果
> Configure project :app
root project 'GradleDemo'

2、getRootDir 方法

获取根目录文件夹路径

println getRootDir()

//打印结果
> Configure project :app
/Users/zhouying/learning/GradleDemo

3、getBuildDir 方法

获取当前 Project 的 build 文件夹路径

println getBuildDir()

//打印结果
> Configure project :app
/Users/zhouying/learning/GradleDemo/app/build

4、getParent 方法

获取当前父 Project 对象

println getParent()

//打印结果
> Configure project :app
root project 'GradleDemo'

5、getAllprojects 方法

获取当前 Project 及其子 Project 对象,返回值是一个 Set 集合

//当前在根工程的 build.gradle 文件下
println getAllprojects()

//打印结果
> Configure project :
[root project 'GradleDemo', project ':app']

我们还可以使用其闭包的形式

allprojects {
    println it
}

//打印结果
> Configure project :
root project 'GradleDemo'
project ':app'

//我们通常会使用闭包的语法在根 build.gradle 下进行相关的配置,如下:
allprojects {
    repositories {
        google()
        mavenCentral()
    }
}

注意:根 Project 与其子 Project 组成了一个树形结构,但这颗树的高度也仅仅被限定为了两层

6、getSubprojects 方法

获取当前 Project 下的所有子 Project 对象,返回值是一个 Set 集合

//当前在根工程的 build.gradle 文件下
println getSubprojects()

//打印结果
> Configure project :
[project ':app']

同样我们也可以使用其闭包的形式

subprojects {
    println it
}

//打印结果
> Configure project :
project ':app'

7、apply 系列方法

引用插件

//引用第三方插件
apply plugin: 'com.android.application'

//引用脚本文件插件
apply from: 'config.gradle'

8、configurations 闭包

编写 Project 一些相关的配置,如全局移除某个依赖

configurations {
    all*.exclude group: '组名'module: '模块名'
}

9、project 系列方法

指定工程实例,然后在闭包中对其进行相关的配置

project("app") {
    apply plugin: 'com.android.application'
}

2)、扩展属性

扩展属性作用:方便我们全局的一个使用。类似 Java 中,在工具类里面定义静态方法

1、扩展属性定义

我们可以通过以下两种方式来定义扩展属性:

1、通过 ext 关键字定义扩展属性

2、在 gradle.properties 下定义扩展属性

1、通过 ext 关键字定义扩展属性

通过 ext 定义扩展属性的语法有两种:

//当前在根 build.gradle 下
//方式1:ext.属性名
ext.test = 'erdai666'

//方式2:ext 后面接上一个闭包
ext{
  test1 = 'erdai777'
}
2、在 gradle.properties 下定义扩展属性

通过 gradle.properties 定义扩展属性,直接使用 key=value 的形式即可:

test2=erdai888

2、扩展属性调用

1、ext 定义的扩展属性调用的时候可以去掉 ext 前缀直接调用

2、ext 定义的扩展属性也可以通过 当前定义扩展属性的 Project 对象.ext.属性名 进行调用

3、gradle.properties 定义的扩展属性直接通过属性名调用即可

下面我们在 app 的 build.gradle 下进行演示:

/**
 * 下面这种写法之所以能这么写
 * 1、ext 定义的扩展属性调用的时候可以去掉 ext 前缀直接调用
 * 2、子 Project 能拿到根 Project 中的属性和方法
 */

println test
println test1
println test2

//2、ext 定义的扩展属性也可以通过 当前定义扩展属性的 Project 对象.ext.属性名 调用
println rootProject.ext.test
println rootProject.ext.test1
println test2

//上述两种方式打印结果均为
> Configure project :app
erdai666
erdai777
erdai888

注意: 子 Project 和根 Project 存在继承关系,因此根 Project 中定义的属性和方法子 Project 能获取到

3、扩展属性应用

通常我们会使用扩展属性来优化 build.gradle 脚本文件,例如我们以优化 app 下的 build.gradle 为例:

首先看一眼优化之前 app 的 build.gradle 长啥样:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 30

    defaultConfig {
        applicationId "com.dream.gradledemo"
        minSdkVersion 19
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.4.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

下面我们就来进行改造

步骤1: 在根目录下创建一个脚本文件 config.gradle ,用来存放扩展属性

ext{

    androidConfig = [
            compileSdkVersion : 30,
            applicationId : 'com.dream.gradledemo',
            minSdkVersion : 19,
            targetSdkVersion : 30,
            versionCode : 1,
            versionName : '1.0'
    ]


    implementationLib = [
            appcompat : 'androidx.appcompat:appcompat:1.3.0',
            material  : 'com.google.android.material:material:1.4.0',
            constraintlayout : 'androidx.constraintlayout:constraintlayout:2.0.4'
    ]

    testImplementationLib = [
            junit : 'junit:junit:4.13.2'
    ]


    androidTestImplementationLib = [
            junit : 'androidx.test.ext:junit:1.1.3',
            'espresso-core' : 'androidx.test.espresso:espresso-core:3.4.0'
    ]
}

步骤2: 在根 build.gradle 对 config.gradle 进行引用

apply from: 'config.gradle'

注意: 在根 build.gradle 进行引用的好处就是所有的子 build.gradle 都能够获取到这些扩展属性

步骤3:  在 app 的 build.gradle 里面进行扩展属性的调用

apply plugin: 'com.android.application'

android {
    compileSdkVersion androidConfig.compileSdkVersion

    defaultConfig {
        applicationId androidConfig.applicationId
        minSdkVersion androidConfig.minSdkVersion
        targetSdkVersion androidConfig.targetSdkVersion
        versionCode androidConfig.versionCode
        versionName androidConfig.versionName

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

def implementationLibMap = implementationLib
def testImplementationLibMap = testImplementationLib
def androidTestImplementationLibMap = androidTestImplementationLib

dependencies {
    implementationLibMap.each{k,v ->
        implementation v
    }

    testImplementationLibMap.each{k,v ->
        testImplementation v
    }

    androidTestImplementationLibMap.each{k,v ->
        androidTestImplementation v
    }
}

3)、文件操作 API

1、file/files 系列文件定位

Project 对象提供的 file/files 系列方法主要用来定位一个或者多个文件,值得注意的是:它们接收的参数是一个相对路径,从当前 project 工程开始查找,而我们通过 new File 的方式需要传入一个绝对路径,下面通过代码演示感受一下他们的区别:

//============================== 1、file 方法应用============================
//通过 file 方法传入一个相对路径,返回值是一个 file 对象
println file('../config.gradle').text

//通过 new File 方式传入一个绝对路径
def file = new File('/Users/zhouying/learning/GradleDemo/config.gradle')
println file.text

//上述两者打印结果相同,如下截图

//============================== 2、files 方法应用============================
//通过 files 方法传入多个相对路径,返回值是一个 ConfigurableFileCollection 即文件集合
files('../config.gradle','../build.gradle').each {
    println it.name
}

//打印结果
> Configure project :app
config.gradle
build.gradle

Gradle 系列 (二)、Gradle 技术探索

2、copy 文件拷贝

1、Project 对象提供了 copy 方法,它使得我们拷贝一个文件或文件夹变得十分简单

2、copy 方法能够接收一个闭包,闭包的参数 CopySpec ,CopySpec 提供了很多文件操作的 API,具体可以查看文档:https://docs.gradle.org/current/javadoc/org/gradle/api/file/CopySpec.html

下面会使用到 CopySpec 的 from 和 into 方法

注意: from 和 into 接收的参数是 Object 类型的,因此我们可以传入一个路径或文件

1、文件拷贝

例如我们实现:将根目录下的 config.gradle 文件拷贝拷贝到 app 目录下。 如下:

//1、传入路径
copy {
    from getRootDir().path + "/config.gradle"
    into getProjectDir().path
}

//2、传入文件
copy {
    from file('../config.gradle')
    into getProjectDir()
}

//最终结果是这两种方式都能拷贝成功

2、文件夹拷贝

例如我们实现:将根目录下的 gradle 文件夹下的所有文件和文件夹拷贝到 app 目录下的 gradle 文件夹

copy {
    from file('../gradle/')
    into getProjectDir().path + "/gradle/"
}

//最终结果拷贝成功

此时如果 app 目录下没有 gradle 文件夹,那么 copy 方法会给我们自动创建,非常的方便

3、fileTree 文件树映射

Project 对象提供了 fileTree 方法,方便我们将一个目录转换为文件树,然后对文件树进行相关的逻辑处理,它接收的参数和 file/files 类似,也是一个相对路径

例如我们实现:遍历根目录下的 gradle 文件夹,并打印文件及文件夹的名称

fileTree('../gradle/'){ FileTree fileTree ->
    fileTree.visit { FileTreeElement fileTreeElement ->
        println fileTreeElement.name
    }
}

//打印结果
> Configure project :app
wrapper
gradle-wrapper.jar
gradle-wrapper.properties

我们通常会在 app 的 build.gradle 下看到这么一个配置语句:

implementation fileTree(include: ['*.jar'], dir: 'libs')

他实际上是调用了 fileTree 接收 Map 参数的重载方法:

ConfigurableFileTree fileTree(Map<String, ?> var1);

这句配置语句的意思就是:引入当前 project 目录下的 libs 文件夹下的所有 jar 包

4)、buildscript 解读

我们通常在新建一个 Android 项目的时候可以看到根 build.gradle 有这么一段配置:

buildscript {
    //插件仓库地址
    repositories {
        google()
        mavenCentral()
    }
  
    //插件依赖
    dependencies {
        classpath "com.android.tools.build:gradle:4.2.1"
    }
}

它的作用是:引入 Gradle 构建过程中的一些插件

实际上上面这段代码的完整写法如下:

buildscript { ScriptHandler scriptHandler ->
    scriptHandler.repositories { RepositoryHandler repositoryHandler ->
        repositoryHandler.google()
        repositoryHandler.mavenCentral()
    }
  
    scriptHandler.dependencies { DependencyHandler dependencyHandler ->
        dependencyHandler.classpath "com.android.tools.build:gradle:4.2.1"
    }
}

你是否会有这么一个疑问:为啥这些参数都能够去掉,简化成上面那样?🤔️

要明白上面这个问题,首先我们得对闭包有一定的了解:

1、首先闭包中有 owenr this delegate 三个对象,这三个对象拥有的属性和方法我们都可以调用,并且无需写出来

2、这三个对象调用的先后顺序取决于闭包的委托策略,一般我们会对 delegate 进行操作并修改它的委托策略

实际上,Gradle 对上面的这些闭包的 delegate 修改为了传入闭包的参数,并把委托策略设置为了 DELEGATE_FIRST ,因此我们调用的时候才能把这些参数给去掉,感兴趣的可以点击 buildscript 进去看下源码,这里就不对源码进行分析了

5)、exec 外部命令执行

Project 对象提供了 exec 方法,方便我们执行外部的命令

我们可以在 Linux 下通过如下命令去移动一个文件夹:

mv -f 源文件路径 目标文件路径

现在我们在 Gradle 下去进行这一操作

例如我们实现:使用外部命令,将我们存放的 apk 目录移动到项目的根目录 ,如下:

task taskMove() {
    doLast {
        // 在 gradle 的执行阶段去执行
        def sourcePath = buildDir.path + "/outputs/apk"
        def destinationPath = getRootDir().path
        def command = "mv -f $sourcePath $destinationPath"
        exec {
            try {
                executable "bash"
                args "-c", command
                println "The command execute is success"
            } catch (GradleException e) {
                e.printStackTrace()
                println "The command execute is failed"
            }
        }
    }
}

三、Task 介绍

Task 中文翻译即任务,它是 Gradle 中的一个接口,代表了要执行的任务,不同的插件可以添加不同的 Task,每一个 Task 都要和 Project关联。众所周知,线程是 cpu 执行的最小单元。同理,Task 是 Gradle 执行的最小单元,Gradle 将一个个 Task 串联起来,完成一个具体的构建任务

1)、doFirst、doLast 介绍

首先我们要搞懂 Action 这个概念,Action 本质上是一个执行动作,它只有在我们执行当前 Task 时才会被执行,Gradle 执行阶段本质上就是在执行每个 Task 中的一系列 Action

doFirst,doLast 是 Task 给我们提供的两个 Action

doFirst 表示:Task 执行最开始时被调用的 Action

doLast 表示:task 执行完时被调用的 Action

值的注意的是:doFirst 和 doLast 可被多次添加执行 ,如下:

task erdai{
    println 'task start...'

    doFirst {
        println 'doFirst1'
    }

    doLast {
        println 'doLast1'
    }

    doLast {
        println 'doLast2'
    }

    println 'task end...'
}

//执行当前 task
./gradlew erdai

//打印结果如下
> Configure project :app
task start...
task end...

Task :app:erdai
doFirst1
doLast1
doLast2

从上述打印结果我们可以发现

1、println 'task start...' println 'task end...'这两句的代码在 Gradle 配置阶段就被执行了

2、doFirst,doLast 中的代码是在 Gradle 执行阶段,执行 erdai 这个 task 时被执行的

因此也验证了一开始我说的那个结论:Gradle 配置阶段,除 Task 的 Action 中编写的代码都会被执行

2)、Task 属性介绍

属性 描述 默认值
name task 名字 无,必须指定
type Task 的父类 DefaultTask
action 当 Task 执行的时候,需要执行的闭包或 Action null
overwrite 替换一个已存在的 Task false
dependsOn 该 Task 所依赖的 Task 集合 []
group 该 task 所属分组 null
description 该 Task 的描述信息 null
constructorArgs 传递到 Task Class 构造器中的参数 null

3)、Task 类型介绍

一般我们创建的 Task 默认是继承 DefaultTask,我们可以通过 type 属性让他继承其他的类,也可以通过 extends 关键字直接指定,Gradle 自带的有  Copy、Delete 等等,如下:

// 1、继承 Delete 这个类,删除根目录下的 build 文件
task deleteTask(type: Delete) {
    delete rootProject.buildDir
}

//通过 extends 关键字指定
class DeleteTask extends Delete{

}
DeleteTask deleteTask = tasks.create("deleteTask",DeleteTask)
deleteTask.delete(rootProject.buildDir)


// 2、继承 Copy 这个类
task copyTask(type: Copy) {
    //...
}

//通过 extends 关键字指定
class CopyTask extends Copy{
    //...
}

4)、TaskContainer 介绍

TaskContainer 你可以理解为一个 Task 容器,Project 对象就是通过 TaskContainer 来管理 Task,因此我们可以通过 TaskContainer,对 Task 进行相关的操作,一些常用的 API 如下:

//查找task
findByPath(path: String): Task 
getByPath(path: String): Task
getByName(name: String): Task

//创建task
create(name: String): Task
create(name: String, configure: Closure): Task 
create(name: String, type: Class): Task
create(options: Map<String, ?>): Task
create(options: Map<String, ?>, configure: Closure): Task

//当 task 被加入到 TaskContainer 时的监听
whenTaskAdded(action: Closure)

5)、Task 定义及配置

因为 Task 和 Project 是相互关联的,Project 中提供了一系列创建 Task 的方法,下面介绍一些常用的:

//1、创建一个名为 task1 的 Task
task task1

//2、创建一个名为 task2 的 Task,并通过闭包进行相应的配置
task task2{
    //指定 task 的分组
    group 'erdai666'
  
    doFirst{
    
    }
}

//3、创建一个名为 task3 的 Task,该 Task 继承自 Copy 这个 Task,依赖 task2
task task3(type: Copy){
    dependsOn "task2"
    doLast{
    
    }
}

//4、创建一个名为 task4 的 Task 并指定了分组和描述
task task4(group: "erdai666"description: "task4") {
    doFirst {
        
    }
    
    doLast {
        
    }
}

//5、通过 Project 对象的 TaskContainer 创建名为 task5 的 Task
tasks.create("task5"){

}

//6、通过 Project 对象的 TaskContainer 创建名为 task6 的 Task
//相对于 5 ,只是调用了不同的重载方法而已
tasks.create(name: "task6"){

}

6)、Task 执行实战

通常我们会使用 doFirst 与 doLast 在 Task 执行期间进行相关操作,下面我们就来实现 build 任务执行期间耗时

// Task 执行实战:计算 build 执行期间的耗时
def startBuildTime, endBuildTime
// 1、在 Gradle 配置阶段完成之后进行操作,
// 以此保证要执行的 task 配置完毕
this.afterEvaluate { Project project ->
    // 2、找到当前 project 下第一个执行的 task,即 preBuild task
    def preBuildTask = project.tasks.getByName("preBuild")
    preBuildTask.doFirst {
        // 3、获取第一个 task 开始执行时刻的时间戳
        startBuildTime = System.currentTimeMillis()
    }
    // 4、找到当前 project 下最后一个执行的 task,即 build task
    def buildTask = project.tasks.getByName("build")
    buildTask.doLast {
        // 5、获取最后一个 task 执行完成前一瞬间的时间戳
        endBuildTime = System.currentTimeMillis()
        // 6、输出 build 执行期间的耗时
        println "Current project execute time is ${endBuildTime - startBuildTime}"
    }
}

//执行 build 任务
./gradlew build

//打印结果
Current project execute time is 21052

7)、指定 Task 执行顺序

在 Gradle 中,有三种方式可以指定 Task 执行顺序:

1、dependsOn 强依赖方式

2、通过 Task 输入输出

3、通过 API 指定执行顺序

1、dependsOn 强依赖方式

dependsOn 强依赖方式可细分为静态依赖动态依赖

  • 静态依赖:在创建 Task 的时候,直接通过 dependsOn 指定需要依赖的 Task
  • 动态依赖:在创建 Task 的时候,不知道需要依赖哪些 Task,需通过 dependsOn 动态依赖符合条件的 Task
//=================================静态依赖=============================
task taskA{
    doLast {
        println 'taskA'
    }
}

task taskB{
    doLast {
        println 'taskB'
    }
}

task taskC(dependsOn: taskA){//多依赖方式 dependsOn:[taskA,taskB]
    doLast {
        println 'taskC'
    }
}

//执行 taskC
./gradlew taskC

//打印结果
Task :app:taskA
taskA

Task :app:taskC
taskC

上述代码,当我们执行 taskC 的时候,因为依赖了 taskA,因此 taskA 会先执行,在执行 taskC

注意:当一个 Task 依赖多个 Task 的时候,被依赖的 Task 之间如果没有依赖关系,那么它们的执行顺序是随机的,并无影响,如下:

task taskC(dependsOn:[taskA,taskB]){
    doLast {
        println 'taskC'
    }
}

taskA 和 taskB 的执行顺序是随机的

//=================================动态依赖=============================
// Task 动态依赖方式
task lib1 {
    doLast{
        println 'lib1'
    }
}
task lib2 {
    doLast{
        println 'lib2'
    }
}
task lib3 {
    doLast{
        println 'lib3'
    }
}

// 动态指定taskX依赖所有以lib开头的task
task taskDynamic{
    // 动态指定依赖
    dependsOn tasks.findAll{ Task task ->
        return task.name.startsWith('lib')
    }

    doLast {
        println 'taskDynamic'
    }
}

//执行 taskDynamic
./gradlew taskDynamic

//打印结果
Task :app:lib1
lib1

Task :app:lib2
lib2

Task :app:lib3
lib3

Task :app:taskDynamic
taskDynamic

2、通过 Task 输入输出指定执行顺序

当一个参数,作为 TaskA 的输入参数,同时又作为 TaskB 的输出参数,那么 TaskA 执行的时候先要执行 TaskB。即输出的 Task 先于输入的 Task 执行

但是我在实际测试过程中发现:输入的 Task 会先执行,然后在执行输出的 Task,如下:

ext {
    testFile = file("${projectDir.path}/test.txt")
    if(testFile != null || !testFile.exists()){
        testFile.createNewFile()
    }
}

//输出 Task
task outputTask {
    outputs.file testFile
    doLast {
        outputs.getFiles().singleFile.withWriter { writer ->
            writer.append("erdai666")
        }
        println "outputTask 执行结束"
    }
}

//输入 Task
task inputTask {
    inputs.file testFile
    doLast {
        println "读取文件内容:${inputs.files.singleFile.text}"
        println "inputTask 执行结束"
    }
}

//测试 Task
task testTask(dependsOn: [outputTask, inputTask]) {
    doLast {
        println "testTask1 执行结束"
    }
}

//执行 testTask
./gradlew testTask

//理论上会先执行 outputTask,在执行 inputTask,最后执行 testTask
//但实际打印结果
Task :app:inputTask
读取文件内容:
inputTask 执行结束

Task :app:outputTask
outputTask 执行结束

Task :app:testTask
testTask1 执行结束

最终我对 inputTask 指定了具体依赖才达到了预期效果:

task inputTask(dependsOn: outputTask) {
    inputs.file testFile
    doLast {
        println "读取文件内容:${inputs.files.singleFile.text}"
        println "inputTask 执行结束"
    }
}

//修改之后的打印结果
Task :app:outputTask
outputTask 执行结束

Task :app:inputTask
读取文件内容:erdai666
inputTask 执行结束

Task :app:testTask
testTask1 执行结束

3、通过 API 指定执行顺序

可以指定 Task 执行顺序的 API 有:

mustRunAfter:指定必须在哪个 Task 执行完成之后执行

shouldRunAfter:跟 mustRunAfter 类似,区别在于不强制,不常用

finalizeBy:在当前 Task 执行完成之后,指定执行的 Task

下面我们通过代码来演示一下:

//======================================= mustRunAfter ===========================
task taskX{
    doLast {
        println 'taskX'
    }
}

task taskY{
    mustRunAfter taskX
    doLast {
        println 'taskY'
    }
}

task taskXY(dependsOn: [taskX,taskY]){
    doLast {
        println 'taskXY'
    }
}

//执行 taskXY
./gradlew taskXY

//打印结果
Task :app:taskX
taskX

Task :app:taskY
taskY

Task :app:taskXY
taskXY

//======================================= finalizeBy ===========================
task taskI{
    doLast {
        println 'taskI'
    }
}

task taskJ{
    finalizedBy taskI
    doLast {
        println 'taskJ'
    }
}


task taskIJ(dependsOn: [taskI,taskJ]){
    doLast {
        println 'taskIJ'
    }
}

//执行 taskIJ
./gradlew taskIJ

//打印结果
Task :app:taskJ
taskJ

Task :app:taskI
taskI

Task :app:taskIJ
taskIJ

四、自定义 Task 挂接到 Android 应用构建流程

1)、Task 依赖关系插件介绍

我们可以引入如下插件来查看 Task 的一个依赖关系:

//1、在根 build.gradle 添加如下代码
buildscript {
    repositories {
       //...
        maven{
           url "https://plugins.gradle.org/m2/"
        }
    }
    dependencies {
       //..
        classpath "gradle.plugin.com.dorongold.plugins:task-tree:1.5"
    }
}


// 2、在 app 的 build.gradle 中应用插件
apply plugin: com.dorongold.gradle.tasktree.TaskTreePlugin

/**
 * 3、在命令行中执行:./gradlew <任务名> taskTree --no-repeat 命令即可查看
 * 这里以执行 build 这个 task 为例
 */

./gradlew build taskTree --no-repeat

经过上面 3 步,我们看下依赖关系图,仅截取部分:

Gradle 系列 (二)、Gradle 技术探索

2)、自定义 Task 挂接到 Android 构建流程

我们知道,Gradle 在执行阶段就是执行 Task 及其依赖的 Task,就比如上面截图的 build Task 的关系依赖图,它会按照这个依赖图有条不紊的去执行。

那么如果我想把自己自定义的 Task 挂接到这个构建流程,该怎么做呢?

1、通过 dependsOn 指定

注意: 单独使用 dependsOn ,必须让构建流程中的 Task 依赖我们自定义的 Task,否则我们的 Task 不会生效

如下代码演示一下:

task myCustomTask{
    doLast {
        println 'This is myCustomTask'
    }
}

afterEvaluate {
    //1、找到需要的构建流程 Task
    def mergeDebugResources = tasks.findByName("mergeDebugResources")
    //2、通过 dependsOn 指定
    mergeDebugResources.dependsOn(myCustomTask)
  
    //如果换成下面这种写法则自定义 Task 不会生效
    //myCustomTask.dependsOn(mergeDebugResources)
}

接下来我们验证一下

首先看一眼 Task 依赖关系图:

Gradle 系列 (二)、Gradle 技术探索

我们自定义的 Task 挂接到了 mergeDebugResources 上

执行下 build 这个 Task,可以发现我们的 Task 被执行了:

Gradle 系列 (二)、Gradle 技术探索

2、通过 finalizedBy 指定

在某个 Task 执行完成后,指定需要执行的 Task

task myCustomTask{
    doLast {
        println 'This is myCustomTask'
    }
}

afterEvaluate {
    def mergeDebugResources = tasks.findByName("mergeDebugResources")
    //将 myCustomTask 挂接在 mergeDebugResources 后面执行
    mergeDebugResources.finalizedBy(myCustomTask)
}
3、通过 mustRunAfter 配合 dependsOn 指定

在两个 Task 之间,插入自定义的 Task

task myCustomTask{
    doLast {
        println 'This is myCustomTask'
    }
}

afterEvaluate {
    //在 mergeDebugResources 和 processDebugResources 之间插入 myCustomTask
    def processDebugResources = tasks.findByName("processDebugResources")
    def mergeDebugResources = tasks.findByName("mergeDebugResources")
    myCustomTask.mustRunAfter(mergeDebugResources)
    processDebugResources.dependsOn(myCustomTask)
}

上述 Task 依赖变化过程:

processDebugResources -> mergeDebugResources ===> processDebugResources -> myCustomTask -> mergeDebugResources

五、Gradle 相关命令介绍

1)、查看项目所有的 Project 对象

./gradlew project

2)、查看 module 下所有的 task

./gradlew $moduleName:tasks

//演示
//查看 app 下的所有 Task
./gradlew app:tasks

//查看根 Project 的所有 Task
./gradlew tasks

3)、执行一个 Task

./gradlew $taskName

//执行 build Task
./gradlew build

4)、查看 module 下的第三方库依赖关系

./gradlew $moduleName:dependencies

//查看 app 下的第三方库依赖关系
./gradlew app:dependencies

六、总结

本篇文章讲的一些重点内容:

1、Gradle 三个阶段及生命周期 Hook 点

2、Project 对象常用 API 介绍,扩展属性的应用与实战

3、Task 常用配置介绍,其中通过 Task 输入输出指定执行顺序遇到了坑:会先执行输入的 Task。最终还是通过使用 dependsOn 指定具体依赖才达到预期效果

4、自定义 Task 挂接到 Android 应用构建流程的三种方式:

1、单独使用 dependsOn (注意必须使用构建流程中的 Task 依赖我们自定义的 Task)

2、使用 finalizedBy

3、mustRunAfter 配合 dependsOn

5、Gradle 一些常用的命令介绍

好了,本篇文章到这里就结束了,希望能给你带来帮助 🤝

感谢你阅读这篇文章

下篇预告

下篇文章我会讲如何自定义第三方插件,敬请期待吧😄

参考和推荐

补齐Android技能树 – 玩转Gradle

Gradle学习系列(二):Gradle核心探索

深度探索 Gradle 自动化构建技术(三、Gradle 核心解密)

从Gradle生命周期到自定义Task挂接到Build构建流程全解

7个你应该知道的Gradle实用技巧

你的点赞,评论和关注是对我巨大的鼓励!

如果有问题,欢迎加我微信:sweetying520 ,在技术学习,个人成长的道路上,我们一起前进!

原文始发于微信公众号(sweetying):Gradle 系列 (二)、Gradle 技术探索

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/122592.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!