Gradle(二)构建流程

972 阅读11分钟

一、Gradle构建流程

1. Gradel构建生命周期

任何Gradle的构建过程都分为三部分:初始化阶段、配置阶段和执行阶段。

初始化阶段

初始化阶段的任务是创建项目的层次结构,并且为每一个项目创建一个Project实例。 与初始化阶段相关的脚本文件是settings.gradle(包括<USER_HOME>/.gradle/init.d目录下的所有.gradle脚本文件,这些文件作用于本机的所有构建过程)。一个settings.gradle脚本对应一个Settings对象,我们最常用来声明项目的层次结构的include就是Settings类下的一个方法,在Gradle初始化的时候会构造一个Settings实例对象,它包含了下图中的方法,这些方法都可以直接在settings.gradle中直接访问。

比如可以通过如下代码向Gradle的构建过程添加监听:

gradle.addBuildListener(new BuildListener() {
  void buildStarted(Gradle var1) {
    println '开始构建'
  }
  void settingsEvaluated(Settings var1) {
    println 'settings评估完成(settins.gradle中代码执行完毕)'
    // var1.gradle.rootProject 这里访问Project对象时会报错,还未完成Project的初始化
  }
  void projectsLoaded(Gradle var1) {
    println '项目结构加载完成(初始化阶段结束)'
    println '初始化结束,可访问根项目:' + var1.gradle.rootProject
  }
  void projectsEvaluated(Gradle var1) {
    println '所有项目评估完成(配置阶段结束)'
  }
  void buildFinished(BuildResult var1) {
    println '构建结束 '
  }
})

编写完相应的 Gradle 生命周期监听代码之后,我们就可以在 Build 输出界面看到如下信息:

settings 评估完成(settings.gradle 中代码执行完毕)
项目结构加载完成(初始化阶段结束)
初始化结束,可访问根项目:root project 'GradleDemo'
所有项目评估完成(配置阶段结束)
> Task :prepareKotlinBuildScriptModel UP-TO-DATE
构建结束
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')
            }
        }
    }
}

结合官网提供的文档传送门去查看,效果杠杠的

配置阶段

配置阶段的任务是执行各项目下的build.gradle脚本,完成Project的配置,并且构造Task任务依赖关系图以便在执行阶段按照依赖关系执行Task。 该阶段也是我们最常接触到的构建阶段,比如应用外部构建插件apply plugin: 'com.android.application',配置插件的属性android{ compileSdkVersion 30 ...}等。每个build.gralde脚本文件对应一个Project对象,在初始化阶段创建,Project接口文档。 配置阶段执行的代码包括build.gralde中的各种语句、闭包以及Task中的配置段语句,在根目录的build.gradle中添加如下代码:

println 'build.gradle的配置阶段'

// 调用Project的dependencies(Closure c)声明项目依赖
dependencies {
    // 闭包中执行的代码
    println 'dependencies中执行的代码'
}

// 创建一个Task
task test() {
  println 'Task中的配置代码'
  // 定义一个闭包
  def a = {
    println 'Task中的配置代码2'
  }
  // 执行闭包
  a()
  doFirst {
    println '这段代码配置阶段不执行'
  }
}

println '我是顺序执行的'

调用gradle build,得到如下结果:

settings 评估完成(settings.gradle 中代码执行完毕)
项目结构加载完成(初始化阶段结束)
初始化结束,可访问根项目:root project 'GradleDemo'

> Configure project :
build.gradle的配置阶段
dependencies中执行的代码
Task中的配置代码
Task中的配置代码2
我是顺序执行的
所有项目评估完成(配置阶段结束)

> Task :prepareKotlinBuildScriptModel UP-TO-DATE
构建结束 

一定要注意,配置阶段不仅执行build.gradle中的语句,还包括了Task中的配置语句。从上面执行结果中可以看到,在执行了dependencies的闭包后,直接执行的是任务test中的配置段代码(Task中除了Action外的代码段都在配置阶段执行)。 另外一点,无论执行Gradle的任何命令,初始化阶段和配置阶段的代码都会被执行。同样是上面那段Gradle脚本,我们执行帮助任务gradle help,任然会打印出上面的执行结果。

执行阶段

在配置阶段结束后,Gradle会根据任务Task的依赖关系创建一个有向无环图,可以通过Gradle对象的getTaskGraph方法访问,对应的类为TaskExecutionGraph,并且,当有向无环图构建完成之后,所有 Task 执行之前,我们可以通过 whenReady(groovy.lang.Closure) 或者 addTaskExecutionGraphListener(TaskExecutionGraphListener) 来接收相应的通知,其代码如下所示:

gradle.getTaskGraph().addTaskExecutionGraphListener(new
TaskExecutionGraphListener() {
    @Override
    void graphPopulated(TaskExecutionGraph graph) {
    }
})

然后通过调用gradle <任务名>执行对应任务。

2. Hook点

Gradle提供了非常多的钩子供开发人员修改构建过程中的行为,为了方便说明,先看下面这张图。 image.png 注意:Gradle 执行脚本文件的时候会生成对应的实例,主要有如下三种对象:

1、Gradle 对象:在项目初始化时构建,全局单例存在,只有这一个对象
2、Project 对象:每一个 build.gradle 都会转换成一个 Project 对象
3、Settings 对象:Seetings.gradle 会转变成一个 Seetings 对象 Gradle 在各个阶段都提供了生命周期回调,在添加监听器的时候需要注意:监听器要在生命周期回调之前添加,否则会导致有些回调收不到

1.Gradle 初始化阶段
  • 在 settings.gradle 执行完后,会回调 Gradle 对象的 settingsEvaluated 方法
  • 在构建所有工程 build.gradle 对应的 Project 对象后,也就是初始化阶段完毕,会回调 Gradle 对象的 projectsLoaded 方法
2.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 执行前后会收到这两个方法的回调

3.执行阶段
  • Gradle 会循环执行 Task 及其依赖的 Task

  • 在当前 Task 执行之前,会回调 TaskExecutionGraph 对象的 beforeTask 方法

  • 在当前 Task 执行之后,会回调 TaskExecutionGraph 对象的 afterTask 方法

  1. 当所有的 Task 执行完毕后,会回调 Gradle 对象的 buildFinish 方法 了解了 Gradle 生命周期后,我们就可以根据自己的需求添加 Hook。例如:我们可以打印 Gradle 构建过程中,各个阶段及各个 Task 的耗时

获取构建各阶段耗时情况

在 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

二、Project

每一个 build.gradle 都有一个与之对应的 Project 实例,而在 build.gradle 中,我们通常都会配置一系列的项目依赖,如下面这个依赖:

implementation 'com.github.bumptech.glide:glide:4.8.0'

类似于 implementation、api 这种依赖关键字,在本质上它就是一个方法调用,在上面,我们使用 implementation() 方法传入了一个 map 参数,参数里面有三对 key-value,完整写法如下所示:

implementation group: 'com.github.bumptech.glide' name:'glide' version:'4.8.0'

当我们使用 implementation、api 依赖对应的 aar 文件时,Gradle 会在 repository 仓库 里面找到与之对应的依赖文件,你的仓库中可能包含 jcenter、maven 等一系列仓库,而每一个仓库其实就是很多依赖文件的集合服务器, 而他们就是通过上述的 group、name、version 来进行归类存储的

1、Project 核心 API 分解

在 Project 中有很多的 API,但是根据它们的 属性和用途 我们可以将其分解为 六大部分,如下图所示: image.png 对于 Project 中各个部分的作用,我们可以先来大致了解下,以便为 Project 的 API 体系建立一个整体的感知能力,如下所示:

  • 1)、Project API让当前的 Project 拥有了操作它的父 Project 以及管理它的子 Project 的能力

  • 2)、Task 相关 API为当前 Project 提供了新增 Task 以及管理已有 Task 的能力

  • 3)、Project 属性相关的 ApiGradle 会预先为我们提供一些 Project 属性,而属性相关的 api 让我们拥有了为 Project 添加额外属性的能力

  • 4)、File 相关 ApiProject File 相关的 API 主要用来操作我们当前 Project 下的一些文件处理

  • 5)、Gradle 生命周期 API即我们在上面讲解过的生命周期 API

  • 6)、其它 API添加依赖、添加配置、引入外部文件等等零散 API 的聚合

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

2、Project API

Project API 文档,我们主要介绍一些常用的 API

1、getRootProject 方法

获取根 Project 对象

println getRootProject()
//运行结果
> Configure project :app
root project 'GradleDemo'

2、getRootDir 方法

获取根目录文件夹路径

println getRootDir()
//运行结果
> Configure project :app
/Users/ex-shenkai001/AndroidStudioProjects/Demo

3、getBuildDir 方法

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

println getBuildDir()
//运行结果
> Configure project :app
/Users/ex-shenkai001/AndroidStudioProjects/Demo/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()
        jcenter()
    }
}

注意:根 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'
}

3、扩展属性

扩展属性作用:方便我们全局的一个使用。类似 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 的形式即可:

// 在 gradle.properties 中
mCompileVersion = 27

// 在 app moudle 下的 build.gradle 中
compileSdkVersion mCompileVersion.toInteger()

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'

步骤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

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

//通过 new File 方式传入一个绝对路径
def file = new File("/Users/ex-shenkai001/AndroidStudioProjects/Demo/config.gradle")
println file.text
//上述两者打印结果相同,如下截图 
//============================== 2、files 方法应用============================ //通过 files 方法传入多个相对路径,返回值是一个 ConfigurableFileCollection 即文件集合
files("config.gradle", "build.gradle").each {
    println it.name
}
//运行结果
> Configure project :
config.gradle
build.gradle

2、copy 文件拷贝

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

2、copy 方法能够接收一个闭包,闭包的参数 CopySpec ,CopySpec 提供了很多文件操作的 API,具体可以查看文档 传送门 下面会使用到 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 {
    // 既可以拷贝文件,也可以拷贝文件夹
    // 这里是将 app moudle 下生成的 apk 目录拷贝到
    // 根工程下的 build 目录
    from file("build/outputs/apk")
    into getRootProject().getBuildDir().path + "/apk/"
    exclude {
        // 排除不需要拷贝的文件
    }
    rename {
        // 对拷贝过来的文件进行重命名
    }
}

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)、其他API

1.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 进去看下源码,这里就不对源码进行分析了

2.app moudle 下的 dependencies

不同于 根项目 buildscript 中的 dependencies 是用来配置我们 Gradle 工程的插件依赖的,而 app moudle 下的 dependencies 是用来为应用程序添加第三方依赖的。关于 app moudle 下的依赖使用这里我们 需要注意下 exclude 与 transitive 的使用 即可,示例代码如下所示:

implementation(rootProject.ext.dependencies.glide) {
        // 排除依赖:一般用于解决资源、代码冲突相关的问题
        exclude module: 'support-v4' 
        // 传递依赖:A => B => C ,B 中使用到了 C 中的依赖,
        // 且 A 依赖于 B,如果打开传递依赖,则 A 能使用到 B 
        // 中所使用的 C 中的依赖,默认都是不打开,即 false
        transitive false 
}

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 中的一系列 Action)。

1. doFirst、doLast 介绍

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

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

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

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

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

task shenkai{
    println 'task start...'

    doFirst {
        println 'doFirst1'
    }

    doLast {
        println 'doLast1'
    }

    doLast {
        println 'doLast2'
    }

    println 'task end...'
}

//执行当前 task
./gradlew shenkai

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

> Task :app:shenkai
doFirst1
doLast1
doLast2

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

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

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

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

2、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"){

}

Task 的属性

需要注意的是,不管是哪一种 task 的定义方式,在 "()" 内我们都可以配置它的一系列属性,如下:

project.task('JsonChao3', group: "JsonChao", description: "my tasks",
dependsOn: ["JsonChao1", "JsonChao2"] ).doLast {
    println "execute JsonChao3 Task"
}

目前 官方所支持的属性 可以总结为如下表格:

选型描述默认值
"name"task 名字无,必须指定
"type"需要创建的 task ClassDefaultTask
"action"当 task 执行的时候,需要执行的闭包 closure 或 行为 Actionnull
"overwrite"替换一个已存在的 taskfalse
"dependsOn"该 task 所依赖的 task 集合[]
"group"该 task 所属组null
"description"task 的描述信息null
"constructorArgs"传递到 task Class 构造器中的参数null

使用 ext 给 task 自定义需要的属性

当然,除了使用已有的属性之外,我们也可以 使用 ext 给 task 自定义需要的属性,代码如下所示:

task Gradle_First() {
    ext.good = true
}

task Gradle_Last() {
    doFirst {
        println Gradle_First.good
    }
    doLast {
        println "I am not $Gradle_First.name"
    }
}

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{
    //...
}

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)

3. Task 的执行详解

Task 通常使用 doFirst 与 doLast 两个方式用于在执行期间进行操作。其示例代码如下所示:

// 使用 Task 在执行阶段进行操作
task myTask3(group: "MyTask", description: "task3") {
    println "This is myTask3"
    doFirst {
        // 老二
        println "This group is 2"
    }

    doLast {
        // 老三
        println "This description is 3"
    }
}

// 也可以使用 taskName.doxxx 的方式添加执行任务
myTask3.doFirst {
    // 这种方式的最先执行 => 老大
    println "This group is 1"
}

Task 执行实战

接下来,我们就使用 doFirst 与 doLast 来进行一下实战,来实现 计算 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

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 步,我们看下依赖关系图,仅截取部分:

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 依赖关系图: 我们自定义的 Task 挂接到了 mergeDebugResources 上

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

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

参考