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

前言

成为一名优秀的Android开发,须要一份完备的知识体系,在这里,让咱们一块儿成长为本身所想的那样~。

从明面上看,Gradle 是一款强大的构建工具,并且许多文章也仅仅都把 Gradle 当作一款工具对待。可是,Gradle 不只仅是一款强大的构建工具,它看起来更像是一个编程框架。Gradle 的组成能够细分为以下三个方面html

  • 1)、 groovy 核心语法包括 groovy 基本语法、闭包、数据结构、面向对象等等
  • 2)、 Android DSL(build scrpit block)Android 插件在 Gradle 所特有的东西,咱们能够在不一样的 build scrpit block 中去作不一样的事情
  • 3)、 Gradle API包含 Project、Task、Setting 等等(本文重点)

能够看到,Gradle 的语法是以 groovy 为基础的,并且,它还有本身独有的 API,因此咱们能够把 Gradle 认做是一款编程框架,利用 Gradle 咱们能够在编程中去实现项目构建过程当中的全部需求。须要注意的是,想要为所欲为地使用 Gradle,咱们必须提早掌握好 groovy,若是对 groovy 还不是很熟悉的建议看看 《深刻探索Gradle自动化构建技术(2、Groovy 筑基篇)》 一文。java

须要注意的是,Groovy 是一门语言,而 DSL 一种特定领域的配置文件,Gradle 是基于 Groovy 的一种框架工具,而 gradlew 则是 gradle 的一个兼容包装工具。android

1、Gradle 优点

一、更好的灵活性

在灵活性上,Gradle 相对于 Maven、Ant 等构建工具, 其 提供了一系列的 API 让咱们有能力去修改或定制项目的构建过程。例如咱们能够 利用 Gradle 去动态修改生成的 APK 包名,可是若是是使用的 Maven、Ant 等工具,咱们就必须等生成 APK 后,再手动去修改 APK 的名称。git

二、更细的粒度

在粒度性上,使用 Maven、Ant 等构建工具时,咱们的源代码和构建脚本是独立的,并且咱们也不知道其内部的处理是怎样的。可是,咱们的 Gradle 则不一样,它 从源代码的编译、资源的编译、再到生成 APK 的过程当中都是一个接一个来执行的github

此外,Gradle 构建的粒度细化到了每个 task 之中。而且它全部的 Task 源码都是开源的,在咱们掌握了这一整套打包流程后,咱们就能够经过去修改它的 Task 去动态改变其执行流程。例如 Tinker 框架的实现过程当中,它经过动态地修改 Gradle 的打包过程生成 APK 的同时,也生成了各类补丁文件。web

三、更好的扩展性

在扩展性上,Gradle 支持插件机制,因此咱们能够复用这些插件,就如同复用库同样简单方便编程

四、更强的兼容性

Gradle 不只自身功能强大,并且它还能 兼容全部的 Maven、Ant 功能,也就是说,Gradle 吸收了全部构建工具的长处json

能够看到,Gradle 相比于其它构建工具,其好处不言而喻,而其 最核心的缘由就是由于 Gradle 是一套编程框架api

2、Gradle 构建生命周期

Gradle 的构建过程分为 三部分:初始化阶段、配置阶段和执行阶段。其构建流程以下图所示:数组

下面分别来详细了解下它们。

一、初始化阶段

首先,在这个阶段中,会读取根工程中的 setting.gradle 中的 include 信息,肯定有多少工程加入构建,而后,会为每个项目(build.gradle 脚本文件)建立一个个与之对应的 Project 实例,最终造成一个项目的层次结构。 与初始化阶段相关的脚本文件是 settings.gradle,而一个 settings.gradle 脚本对应一个 Settings 对象,咱们最经常使用来声明项目的层次结构的 include 就是 Settings 对象下的一个方法,在 Gradle 初始化的时候会构造一个 Settings 实例对象,以执行各个 Project 的初始化配置

settings.gradle

settings.gradle 文件中,咱们能够 在 Gradle 的构建过程当中添加各个生命周期节点监听,其代码以下所示:

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

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

Executing tasks: [clean, :app:assembleSpeedDebug] in project
/Users/quchao/Documents/main-open-project/Awesome-WanAndroid
settings评估完成(settins.gradle中代码执行完毕)
项目结构加载完成(初始化阶段结束)
初始化结束,可访问根项目:root project 'Awesome-WanAndroid'
Configuration on demand is an incubating feature.
> Configure project :app
gradlew version > 4.0
WARNING: API 'variant.getJavaCompiler()' is obsolete and has been
replaced with 'variant.getJavaCompileProvider()'.
It will be removed at the end of 2019.
For more information, see
https://d.android.com/r/tools/task-configuration-avoidance.
To determine what is calling variant.getJavaCompiler(), use
-Pandroid.debug.obsoleteApi=true on the command line to display more
information.
skip tinyPicPlugin Task!!!!!!
skip tinyPicPlugin Task!!!!!!
全部项目评估完成(配置阶段结束)
Task :clean UP-TO-DATE
:clean spend 1ms
...
Task :app:clean
:app:clean spend 2ms
Task :app:packageSpeedDebug
:app:packageSpeedDebug spend 825ms
Task :app:assembleSpeedDebug
:app:assembleSpeedDebug spend 1ms
构建结束 
Tasks spend time > 50ms:
    ...
复制代码

此外,在 settings.gradle 文件中,咱们能够指定其它 project 的位置,这样就能够将其它外部工程中的 moudle 导入到当前的工程之中了。示例代码以下所示:

if (useSpeechMoudle) {
    // 导入其它 App 的 speech 语音模块
    include "speech"
    project(":speech").projectDir = new     File("../OtherApp/speech")
}
复制代码

二、配置阶段

配置阶段的任务是 执行各项目下的 build.gradle 脚本,完成 Project 的配置,与此同时,会构造 Task 任务依赖关系图以便在执行阶段按照依赖关系执行 Task。而在配置阶段执行的代码一般来讲都会包括如下三个部分的内容,以下所示:

  • 1)、 build.gralde 中的各类语句
  • 2)、 闭包
  • 3)、 Task 中的配置段语句

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

三、执行阶段

在配置阶段结束后,Gradle 会根据各个任务 Task 的依赖关系来建立一个有向无环图,咱们能够经过 Gradle 对象的 getTaskGraph 方法来获得该有向无环图 => TaskExecutionGraph,而且,当有向无环图构建完成以后,全部 Task 执行以前,咱们能够经过 whenReady(groovy.lang.Closure) 或者 addTaskExecutionGraphListener(TaskExecutionGraphListener) 来接收相应的通知,其代码以下所示:

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

而后,Gradle 构建系统会经过调用 gradle <任务名> 来执行相应的各个任务。

四、Hook Gradle 各个生命周期节点

这里借用 Goe_H 的 Gradle 生命周期时序图来说解一下 Gradle 生命周期的整个流程,以下图所示:

能够看到,整个 Gradle 生命周期的流程包含以下 四个部分

  • 1)、首先, 解析 settings.gradle 来获取模块信息,这是初始化阶段
  • 2)、而后, 配置每一个模块,配置的时候并不会执行 task
  • 3)、接着, 配置完了之后,有一个重要的回调 project.afterEvaluate,它表示全部的模块都已经配置完了,能够准备执行 task 了
  • 4)、最后, 执行指定的 task 及其依赖的 task

在 Gradle 构建命令中,最为复杂的命令能够说是 gradle build 这个命令了,由于项目的构建过程当中须要依赖不少其它的 task。这里,咱们以 Java 项目的构建过程看看它所依赖的 tasks 及其组成的有向无环图,以下所示:

注意事项

  • 1)、 每个 Hook 点对应的监听器必定要在回调的生命周期以前添加
  • 2)、 若是注册了多个 project.afterEvaluate 回调,那么执行顺序将与注册顺序保持一致

五、获取构建各个阶段、任务的耗时状况

了解了 Gradle 生命周期中的各个 Hook 方法以后,咱们就能够 利用它们来获取项目构建各个阶段、任务的耗时状况,在 settings.gradle 中加入以下代码便可:

long beginOfSetting = System.currentTimeMillis()
def beginOfConfig
def configHasBegin = false
def beginOfProjectConfig = new HashMap()
def beginOfProjectExcute
gradle.projectsLoaded {
    println '初始化阶段,耗时:' + (System.currentTimeMillis() -
beginOfSetting) + 'ms'
}
gradle.beforeProject { project ->
    if (!configHasBegin) {
        configHasBegin = true
        beginOfConfig = System.currentTimeMillis()
    }
    beginOfProjectConfig.put(project, System.currentTimeMillis())
}
gradle.afterProject { project ->
    def begin = beginOfProjectConfig.get(project)
    println '配置阶段,' + project + '耗时:' +
(System.currentTimeMillis() - begin) + 'ms'
}
gradle.taskGraph.whenReady {
    println '配置阶段,总共耗时:' + (System.currentTimeMillis() -
beginOfConfig) + 'ms'
    beginOfProjectExcute = System.currentTimeMillis()
}
gradle.taskGraph.beforeTask { task ->
    task.doFirst {
        task.ext.beginOfTask = System.currentTimeMillis()
    }
    task.doLast {
        println '执行阶段,' + task + '耗时:' +
(System.currentTimeMillis() - task.beginOfTask) + 'ms'
    }
}
gradle.buildFinished {
    println '执行阶段,耗时:' + (System.currentTimeMillis() -
beginOfProjectExcute) + 'ms'
}
复制代码

在 Gradle 中,执行每一种类型的配置脚本就会建立与之对应的实例,而在 Gradle 中如 三种类型的配置脚本,以下所示:

  • 1)、 Build Scrpit对应一个 Project 实例,即每一个 build.gradle 都会转换成一个 Project 实例
  • 2)、 Init Scrpit对应一个 Gradle 实例,它在构建初始化时建立,整个构建执行过程当中以单例形式存在
  • 3)、 Settings Scrpit对应一个 Settings 实例,即每一个 settings.gradle 都会转换成一个 Settings 实例

能够看到,一个 Gradle 构建流程中会由一至多个 project 实例构成,而每个 project 实例又是由一至多个 task 构成。下面,咱们就来认识下 Project。

3、Project

Project 是 Gradle 构建整个应用程序的入口,因此它很是重要,咱们必须对其有深入地了解。不幸的是,网上几乎没有关于 project 讲解的比较好的文章,不过不要紧,下面,咱们将会一块儿来深刻学习 project api 这部分。

由前可知,每个 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 来进行归类存储的

一、Project 核心 API 分解

在 Project 中有不少的 API,可是根据它们的 属性和用途 咱们能够将其分解为 六大部分,以下图所示:

对于 Project 中各个部分的做用,咱们能够先来大体了解下,以便为 Project 的 API 体系创建一个总体的感知能力,以下所示:

  • 1)、 Project API让当前的 Project 拥有了操做它的父 Project 以及管理它的子 Project 的能力
  • 2)、 Task 相关 API为当前 Project 提供了新增 Task 以及管理已有 Task 的能力。因为 task 很是重要,咱们将放到第四章来进行讲解
  • 3)、 Project 属性相关的 ApiGradle 会预先为咱们提供一些 Project 属性,而属性相关的 api 让咱们拥有了为 Project 添加额外属性的能力
  • 4)、 File 相关 ApiProject File 相关的 API 主要用来操做咱们当前 Project 下的一些文件处理
  • 5)、 Gradle 生命周期 API即咱们在第二章讲解过的生命周期 API
  • 6)、 其它 API添加依赖、添加配置、引入外部文件等等零散 API 的聚合

二、Project API

每个 Groovy 脚本都会被编译器编译成 Script 字节码,而每个 build.gradle 脚本都会被编译器编译成 Project 字节码,因此咱们在 build.gradle 中所写的一切逻辑都是在 Project 类内进行书写的。下面,咱们将按照由易到难的套路来介绍 Project 的一系列重要的 API。

须要提早说明的是,默认状况下咱们选定根工程的 build.gradle 这个脚本文件中来学习 Project 的一系列用法,关于 getAllProject 的用法以下所示:

一、getAllprojects

getAllprojects 表示 获取全部 project 的实例,示例代码以下所示:

/**
 * getAllProjects 使用示例
 */

this.getProjects()

def getProjects() {
    println "<================>"
    println " Root Project Start "
    println "<================>"
    // 一、getAllprojects 方法返回一个包含根 project 与其子 project 的 Set 集合
    // eachWithIndex 方法用于遍历集合、数组等可迭代的容器,
    // 并同时返回下标,不一样于 each 方法仅返回 project
    this.getAllprojects().eachWithIndex { Project project, int index ->
        // 二、下标为 0,代表当前遍历的是 rootProject
        if (index == 0) {
            println "Root Project is $project"
        } else {
            println "child Project is $project"
        }
    }
}
复制代码

首先,咱们使用了 def 关键字定义了一个 getProjects 方法。而后,在注释1处,咱们调用了 getAllprojects 方法返回一个包含根 project 与其子 project 的 Set 集合,并链式调用了 eachWithIndex 遍历 Set 集合。接着,在注释2处,咱们会判断当前的下标 index 是不是0,若是是,则代表当前遍历的是 rootProject,则输出 rootProject 的名字,不然,输出 child project 的名字。

下面,咱们在命令行执行 ./gradlew clean,其运行结果以下所示:

quchao@quchaodeMacBook-Pro Awesome-WanAndroid % ./gradlew clean
settings 评估完成(settings.gradle 中代码执行完毕)
项目结构加载完成(初始化阶段结束)
初始化结束,可访问根项目:root project 'Awesome-WanAndroid'
初始化阶段,耗时:5ms
Configuration on demand is an incubating feature.
> Configure project :
<================>
 Root Project Start 
<================>
Root Project is root project 'Awesome-WanAndroid'
child Project is project ':app'
配置阶段,root project 'Awesome-WanAndroid'耗时:284ms
> Configure project :app
...
配置阶段,总共耗时:428ms
> Task :app:clean
执行阶段,task ':app:clean'耗时:1ms
:app:clean spend 2ms
构建结束 
Tasks spend time > 50ms:
执行阶段,耗时:9ms
复制代码

能够看到,执行了初始化以后,就会先配置咱们的 rootProject,并输出了对应的工程信息。接着,便会执行子工程 app 的配置。最后,执行了 clean 这个 task。

须要注意的是,rootProject 与其旗下的各个子工程组成了一个树形结构,可是这颗树的高度也仅仅被限定为了两层

二、getSubprojects

getSubprojects 表示获取当前工程下全部子工程的实例,示例代码以下所示:

/**
 * getAllsubproject 使用示例
 */

this.getSubProjects()

def getSubProjects() {
    println "<================>"
    println " Sub Project Start "
    println "<================>"
    // getSubprojects 方法返回一个包含子 project 的 Set 集合
    this.getSubprojects().each { Project project ->
        println "child Project is $project"
    }
}
复制代码

同 getAllprojects 的用法同样,getSubprojects 方法返回了一个包含子 project 的 Set 集合,这里咱们直接使用 each 方法将各个子 project 的名字打印出来。其运行结果以下所示:

quchao@quchaodeMacBook-Pro Awesome-WanAndroid % ./gradlew clean
settings 评估完成(settings.gradle 中代码执行完毕)
...
> Configure project :
<================>
 Sub Project Start 
<================>
child Project is project ':app'
配置阶段,root project 'Awesome-WanAndroid'耗时:289ms
> Configure project :app
...
全部项目评估完成(配置阶段结束)
配置阶段,总共耗时:425ms
> Task :app:clean
执行阶段,task ':app:clean'耗时:1ms
:app:clean spend 2ms
构建结束 
Tasks spend time > 50ms:
执行阶段,耗时:9ms
复制代码

能够看到,一样在 Gradle 的配置阶段输出了子工程的名字。

三、getParent

getParent 表示 获取当前 project 的父类,须要注意的是,若是咱们在根工程中使用它,获取的父类会为 null,由于根工程没有父类,因此这里咱们直接在 app 的 build.gradle 下编写下面的示例代码:

...
> Configure project :
配置阶段,root project 'Awesome-WanAndroid'耗时:104ms
> Configure project :app
gradlew version > 4.0
my parent project is Awesome-WanAndroid
配置阶段,project ':app'耗时:282ms
...
全部项目评估完成(配置阶段结束)
配置阶段,总共耗时:443ms
...
复制代码

能够看到,这里输出了 app project 当前的父类,即 Awesome-WanAndroid project。

四、getRootProject

若是咱们想在根工程仅仅获取当前的 project 实例该怎么办呢?直接使用 getRootProject 便可在任意 build.gradle 文件获取当前根工程的 project 实例,示例代码以下所示:

/**
 * 四、getRootProject 使用示例
 */

this.getRootPro()

def getRootPro() {
    def rootProjectName = this.getRootProject().name
    println "root project is $rootProjectName"
}
复制代码

五、project

project 表示的是 指定工程的实例,而后在闭包中对其进行操做。在使用以前,咱们有必要看看 project 方法的源码,以下所示:

    /**
     * <p>Locates a project by path and configures it using the given closure. If the path is relative, it is
     * interpreted relative to this project. The target project is passed to the closure as the closure's delegate.</p>
     *
     * @param path The path.
     * @param configureClosure The closure to use to configure the project.
     * @return The project with the given path. Never returns null.
     * @throws UnknownProjectException If no project with the given path exists.
     */

    Project project(String path, Closure configureClosure);
复制代码

能够看到,在 project 方法中两个参数,一个是指定工程的路径,另外一个是用来配置该工程的闭包。下面咱们看看如何灵活地使用 project,示例代码以下所示:

/**
 * 五、project 使用示例
 */


// 一、闭包参数能够放在括号外面
project("app") { Project project ->
    apply plugin: 'com.android.application'
}

// 二、更简洁的写法是这样的:省略参数
project("app") {
    apply plugin: 'com.android.application'
}
复制代码

使用熟练以后,咱们一般会采用注释2处的写法。

六、allprojects

allprojects 表示 用于配置当前 project 及其旗下的每个子 project,以下所示:

/**
 * 六、allprojects 使用示例
 */


// 同 project 同样的更简洁写法
allprojects {
    repositories {
        google()
        jcenter()
        mavenCentral()
        maven {
            url "https://jitpack.io"
        }
        maven { url "https://plugins.gradle.org/m2/" }
    }
}
复制代码

在 allprojects 中咱们通常用来配置一些通用的配置,好比上面最多见的全局仓库配置。

七、subprojects

subprojects 能够 统一配置当前 project 下的全部子 project,示例代码以下所示:

/**
 * 七、subprojects 使用示例:
 *    给全部的子工程引入 将 aar 文件上传置 Maven 服务器的配置脚本
 */

subprojects {
    if (project.plugins.hasPlugin("com.android.library")) {
        apply from: '../publishToMaven.gradle'
    }
}
复制代码

在上述示例代码中,咱们会先判断当前 project 旗下的子 project 是否是库,若是是库才有必要引入 publishToMaven 脚本。

三、project 属性

目前,在 project 接口里,仅仅预先定义了 七个 属性,其源码以下所示:

public interface Project extends Comparable<Project>, ExtensionAwarePluginAware {
    /**
     * 默认的工程构建文件名称
     */

    String DEFAULT_BUILD_FILE = "build.gradle";

    /**
     * 区分开 project 名字与 task 名字的符号
     */

    String PATH_SEPARATOR = ":";

    /**
     * 默认的构建目录名称
     */

    String DEFAULT_BUILD_DIR_NAME = "build";

    String GRADLE_PROPERTIES = "gradle.properties";

    String SYSTEM_PROP_PREFIX = "systemProp";

    String DEFAULT_VERSION = "unspecified";

    String DEFAULT_STATUS = "release";
    
    ...
}
复制代码

幸运的是,Gradle 提供了 ext 关键字让咱们有能力去定义自身所须要的扩展属性。有了它即可以对咱们工程中的依赖进行全局配置。下面,咱们先从配置的远古时代讲起,以便让咱们对 gradle 的 全局依赖配置有更深刻的理解。

ext 扩展属性

一、远古时代

在 AS 刚出现的时候,咱们的依赖配置代码是这样的:

android {
    compileSdkVersion 27
    buildToolsVersion "28.0.3"
    ...
}
复制代码

二、刀耕火种

可是这种直接写值的方式显示是不规范的,所以,后面咱们使用了这种方式:

def mCompileSdkVersion = 27
def mBuildToolsVersion = "28.0.3"

android {
    compileSdkVersion mCompileSdkVersion
    buildToolsVersion mBuildToolsVersion
    ...
}
复制代码

三、铁犁牛耕

若是每个子 project 都须要配置相同的 Version,咱们就须要多写不少的重复代码,所以,咱们能够利用上面咱们学过的 subproject 和 ext 来进行简化:

// 在根目录下的 build.gradle 中
subprojects {
    ext {
        compileSdkVersion = 27
        buildToolsVersion = "28.0.3"
    }
}

// 在 app moudle 下的 build.gradle 中
android {
    compileSdkVersion this.compileSdkVersion
    buildToolsVersion this.buildToolsVersion
    ...
}
复制代码

四、工业时代

使用 subprojects 方法来定义通用的扩展属性仍是存在着很严重的问题,它跟以前的方式同样,仍是会在每个子 project 去定义这些被扩展的属性,此时,咱们能够将 subprojects 去除,直接使用 ext 进行全局定义便可:

// 在根目录下的 build.gradle 中
ext {
    compileSdkVersion = 27
    buildToolsVersion = "28.0.3"
}
复制代码

五、电器时代

当项目愈来愈大的时候,在根项目下定义的 ext 扩展属性愈来愈多,所以,咱们能够将这一套全局属性配置在另外一个 gradle 脚本中进行定义,这里咱们一般会将其命名为 config.gradle,通用的模板以下所示:

ext {

    android = [
            compileSdkVersion       : 27,
            buildToolsVersion       : "28.0.3",
            ...
            ]
            
    version = [
            supportLibraryVersion   : "28.0.0",
            ...
            ]
            
    dependencies = [
            // base
            "appcompat-v7"                      : "com.android.support:appcompat-v7:${version["supportLibraryVersion"]}",
            ...
            ]
            
    annotationProcessor = [
            "glide_compiler"                    : "com.github.bumptech.glide:compiler:${version["glideVersion"]}",
            ...
            ]
            
    apiFileDependencies = [
            "launchstarter"                                   : "libs/launchstarter-release-1.0.0.aar",
            ...
            ]
            
    debugImplementationDependencies = [
            "MethodTraceMan"                                  : "com.github.zhengcx:MethodTraceMan:1.0.7"
    ]

    releaseImplementationDependencies = [
            "MethodTraceMan"                                  : "com.github.zhengcx:MethodTraceMan:1.0.5-noop"
    ]
    
    ...
}
复制代码

六、更加智能化的如今

尽管有了很全面的全局依赖配置文件,可是,在咱们的各个模块之中,仍是不得不写一大长串的依赖代码,所以,咱们能够 使用遍历的方式去进行依赖,其模板代码以下所示:


// 在各个 moulde 下的 build.gradle 脚本下
def implementationDependencies = rootProject.ext.dependencies
def processors = rootProject.ext.annotationProcessor
def apiFileDependencies = rootProject.ext.apiFileDependencies

// 在各个 moulde 下的 build.gradle 脚本的 dependencies 闭包中
// 处理全部的 aar 依赖
apiFileDependencies.each { k, v -> api files(v)}

// 处理全部的 xxximplementation 依赖
implementationDependencies.each 
{ k, v -> implementation v }
debugImplementationDependencies.each { k, v -> debugImplementation v } 
...

// 处理 annotationProcessor 依赖
processors.each { k, v -> annotationProcessor v }

// 处理全部包含 exclude 的依赖
debugImplementationExcludes.each { entry ->
    debugImplementation(entry.key) {
        entry.value.each { childEntry ->
            exclude(group: childEntry.key, module: childEntry.value)
        }
    }
}
复制代码

也许将来随着 Gradle 的不断优化会有更加简洁的方式,若是你有更好地方式,咱们能够来探讨一番。

在 gradle.properties 下定义扩展属性

除了使用 ext 扩展属性定义额外的属性以外,咱们也能够在 gradle.properties 下定义扩展属性,其示例代码以下所示:

// 在 gradle.properties 中
mCompileVersion = 27

// 在 app moudle 下的 build.gradle 中
compileSdkVersion mCompileVersion.toInteger()
复制代码

四、文件相关 API

在 gradle 中,文件相关的 API 能够总结为以下 两大类

  • 1)、 路径获取 API
    • getRootDir()
    • getProjectDir()
    • getBuildDir()
  • 2)、 文件操做相关 API
    • 文件定位
    • 文件拷贝
    • 文件树遍历

1)、路径获取 API

关于路径获取的 API 经常使用的有 三种,其示例代码以下所示:

/**
 * 一、路径获取 API
 */

println "the root file path is:" + getRootDir().absolutePath
println "this build file path is:" + getBuildDir().absolutePath
println "this Project file path is:" + getProjectDir().absolutePath
复制代码

而后,咱们执行 ./gradlew clean,输出结果以下所示:

> Configure project :
the root file path is:/Users/quchao/Documents/main-open-project/Awesome-WanAndroid
this build file path is:/Users/quchao/Documents/main-open-project/Awesome-WanAndroid/build
this Project file path is:/Users/quchao/Documents/main-open-project/Awesome-WanAndroid
配置阶段,root project 'Awesome-WanAndroid'耗时:538ms
复制代码

2)、文件操做相关 API

一、文件定位

经常使用的文件定位 API 有 file/files,其示例代码以下所示:

// 在 rootProject 下的 build.gradle 中

/**
 * 一、文件定位之 file
 */

this.getContent("config.gradle")

def getContent(String path) {
    try {
        // 不一样与 new file 的须要传入 绝对路径 的方式,
        // file 从相对于当前的 project 工程开始查找
        def mFile = file(path)
        println mFile.text 
    } catch (GradleException e) {
        println e.toString()
        return null
    }
}

/**
 * 一、文件定位之 files
 */

this.getContent("config.gradle""build.gradle")

def getContent(String path1, String path2) {
    try {
        // 不一样与 new file 的须要传入 绝对路径 的方式,
        // file 从相对于当前的 project 工程开始查找
        def mFiles = files(path1, path2)
        println mFiles[0].text + mFiles[1].text
    } catch (GradleException e) {
        println e.toString()
        return null
    }
}
复制代码

二、文件拷贝

经常使用的文件拷贝 API 为 copy,其示例代码以下所示:

/**
 * 二、文件拷贝
 */

copy {
    // 既能够拷贝文件,也能够拷贝文件夹
    // 这里是将 app moudle 下生成的 apk 目录拷贝到
    // 根工程下的 build 目录
    from file("build/outputs/apk")
    into getRootProject().getBuildDir().path + "/apk/"
    exclude 
{
        // 排除不须要拷贝的文件
    }
    rename {
        // 对拷贝过来的文件进行重命名
    }
}
复制代码

三、文件树遍历

咱们能够 使用 fileTree 将当前目录转换为文件数的形式,而后即可以获取到每个树元素(节点)进行相应的操做,其示例代码以下所示:

/**
 * 三、文件树遍历
 */

fileTree("build/outputs/apk") { FileTree fileTree ->
    fileTree.visit { FileTreeElement fileTreeElement ->
        println "The file is $fileTreeElement.file.name"
        copy {
            from fileTreeElement.file
            into getRootProject().getBuildDir().path + "/apkTree/"
        }
    }
}
复制代码

五、其它 API

一、依赖相关 API

根项目下的 buildscript

buildscript 中 用于配置项目核心的依赖。其原始的使用示例与简化后的使用示例分别以下所示:

原始的使用示例
buildscript { ScriptHandler scriptHandler ->
    // 配置咱们工程的仓库地址
    scriptHandler.repositories { RepositoryHandler repositoryHandler ->
        repositoryHandler.google()
        repositoryHandler.jcenter()
        repositoryHandler.mavenCentral()
        repositoryHandler.maven { url 'https://maven.google.com' }
        repositoryHandler.maven { url "https://plugins.gradle.org/m2/" }
        repositoryHandler.maven {
            url uri('../PAGradlePlugin/repo')
        }
        // 访问本地私有 Maven 服务器
        repositoryHandler.maven 
{
            name "personal"
            url "http://localhost:8081:/JsonChao/repositories"
            credentials {
                username = "JsonChao"
                password = "123456"
            }
        }
    }
    
      // 配置咱们工程的插件依赖
    dependencies { DependencyHandler dependencyHandler ->
        dependencyHandler.classpath 'com.android.tools.build:gradle:3.1.4'
       
        ...
    }
复制代码
简化后的使用示例
buildscript {
    // 配置咱们工程的仓库地址
    repositories {
        google()
        jcenter()
        mavenCentral()
        maven { url 'https://maven.google.com' }
        maven { url "https://plugins.gradle.org/m2/" }
        maven {
            url uri('../PAGradlePlugin/repo')
        }
    }
    
    // 配置咱们工程的插件依赖
    dependencies 
{
        classpath 'com.android.tools.build:gradle:3.1.4'
        
        ...
    }
复制代码

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 
}
复制代码

二、外部命令执行

咱们通常是 使用 Gradle 提供的 exec 来执行外部命令,下面咱们就使用 exec 命令来 将当前工程下新生产的 APK 文件拷贝到 电脑下的 Downloads 目录中,示例代码以下所示:

/**
 * 使用 exec 执行外部命令
 */

task apkMove() {
    doLast {
        // 在 gradle 的执行阶段去执行
        def sourcePath = this.buildDir.path + "/outputs/apk/speed/release/"
        def destinationPath = "/Users/quchao/Downloads/"
        def command = "mv -f $sourcePath $destinationPath"
        exec {
            try {
                executable "bash"
                args "-c", command
                println "The command execute is success"
            } catch (GradleException e) {
                println "The command execute is failed"
            }
        }
    }
}
复制代码

4、Task

只有 Task 才能够在 Gradle 的执行阶段去执行(其实质是执行的 Task 中的一系列 Action),因此 Task 的重要性不言而喻。

一、从一个例子 🌰 出发

首先,咱们能够在任意一个 build.gradle 文件中能够去定义一个 Task,下面是一个完整的示例代码:

// 一、声明一个名为 JsonChao 的 gradle task
task JsonChao
JsonChao {
    // 二、在 JsonChao task 闭包内输出 hello~,
    // 执行在 gradle 生命周期的第二个阶段,即配置阶段。
    println("hello~")
    // 三、给 task 附带一些 执行动做(Action),执行在
    // gradle 生命周期的第三个阶段,即执行阶段。
    doFirst {
        println("start")
    }
    doLast {
        println("end")
    }
}
// 四、除了上述这种将声明与配置、Action 分别定义
// 的方式以外,也能够直接将它们结合起来。
// 这里咱们又定义了一个 Android task,它依赖于 JsonChao
// task,也就是说,必须先执行完 JsonChao task,才能
// 去执行 Android task,由此,它们之间便组成了一个
// 有向无环图:JsonChao task => Android task
task Andorid(dependsOn:"JsonChao") {
    doLast {
        println("end?")
    }
}
复制代码

首先,在注释1处,咱们声明了一个名为 JsonChao 的 gradle task。接着,在注释2处,在 JsonChao task 闭包内输出了 hello~,这里的代码将会执行在 gradle 生命周期的第二个阶段,即配置阶段。而后,在注释3处,这里 给 task 附带一些了一些执行动做(Action),即 doFirst 与 doLast,它们闭包内的代码将执行在 gradle 生命周期的第三个阶段,即执行阶段

对于 doFirst 与 doLast 这两个 Action,它们的做用分别以下所示:

  • doFirst表示 task 执行最开始的时候被调用的 Action
  • doLast表示 task 将执行完的时候被调用的 Action

须要注意的是,doFirst 和 doLast 是能够被执行屡次的

最后,注释4处,咱们能够看到,除了注释一、二、3处这种将声明与配置、Action 分别定义的方式以外,也能够直接将它们结合起来。在这里咱们又定义了一个 Android task,它依赖于 JsonChao task,也就是说,必须先执行完 JsonChao task,才能 去执行 Android task,由此,它们之间便组成了一个 有向无环图:JsonChao task => Android task

执行 Android 这个 gradle task 能够看到以下输出结果:

> Task :JsonChao
start
end
执行阶段,task ':JsonChao'耗时:1ms
:JsonChao spend 4ms
> Task :Andorid
end?
执行阶段,task ':Andorid'耗时:1ms
:Andorid spend 2ms
构建结束 
Tasks spend time > 50ms:
执行阶段,耗时:15ms
复制代码

二、Task 的定义及配置

Task 常见的定义方式有 两种,示例代码以下所示:

// Task 定义方式1:直接经过 task 函数去建立(在 "()" 能够不指定 group 与 description 属性)
task myTask1(group: "MyTask", description: "task1") {
    println "This is myTask1"
}

// Task 定义方式2:经过 TaskContainer 去建立 task
this.tasks.create(name: "myTask2") {
    setGroup("MyTask")
    setDescription("task2")
    println "This is myTask2"
}
复制代码

定义完上述 Task 以后再同步项目,便可看到对应的 Task Group 及其旗下的 Tasks,以下图所示:

Task 的属性

须要注意的是,无论是哪种 task 的定义方式,在 "()" 内咱们均可以配置它的一系列属性,以下:

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

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

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

使用 "$" 来引用另外一个 task 的属性

在这里,咱们能够 在当前 task 中使用 "$" 来引用另外一个 task 的属性,示例代码以下所示:

task Gradle_First() {

}

task Gradle_Last() {
    doLast {
        println "I am not $Gradle_First.name"
    }
}
复制代码

使用 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"
    }
}
复制代码

使用 defaultTasks 关键字标识默认执行任务

此外,咱们也能够 使用 defaultTasks 关键字 来将一些任务标识为默认的执行任务,代码以下所示:

defaultTasks "Gradle_First""Gradle_Last"

task Gradle_First() {
    ext.good = true
}

task Gradle_Last() {
    doFirst {
        println Gradle_First.goodg
    }
    doLast {
        println "I am not $Gradle_First.name"
    }
}
复制代码

注意事项

每一个 task 都会经历 初始化、配置、执行 这一套完整的生命周期流程

三、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
// 一、在 Gradle 配置阶段完成以后进行操做,
// 以此保证要执行的 task 配置完毕
this.afterEvaluate { Project project ->
    // 二、找到当前 project 下第一个执行的 task,即 preBuild task
    def preBuildTask = project.tasks.getByName("preBuild")
    preBuildTask.doFirst {
        // 三、获取第一个 task 开始执行时刻的时间戳
        startBuildTime = System.currentTimeMillis()
    }
    // 四、找到当前 project 下最后一个执行的 task,即 build task
    def buildTask = project.tasks.getByName("build")
    buildTask.doLast {
        // 五、获取最后一个 task 执行完成前一瞬间的时间戳
        endBuildTime = System.currentTimeMillis()
        // 六、输出 build 执行期间的耗时
        println "Current project execute time is ${endBuildTime - startBuildTime}"
    }
}
复制代码

四、Task 的依赖和执行顺序

指定 Task 的执行顺序有 三种 方式,以下图所示:

1)、dependsOn 强依赖方式

dependsOn 强依赖的方式能够细分为 静态依赖和动态依赖,示例代码以下所示:

静态依赖

task task1 {
    doLast {
        println "This is task1"
    }
}

task task2 {
    doLast {
        println "This is task2"
    }
}

// Task 静态依赖方式1 (经常使用)
task task3(dependsOn: [task1, task2]) {
    doLast {
        println "This is task3"
    }
}

// Task 静态依赖方式2
task3.dependsOn(task1, task2)
复制代码

动态依赖

// Task 动态依赖方式
task dytask4 {
    dependsOn this.tasks.findAll { task ->
        return task.name.startsWith("task")
    }
    doLast {
        println "This is task4"
    }
}
复制代码

2)、经过 Task 指定输入输出

咱们也能够经过 Task 来指定输入输出,使用这种方式咱们能够 高效地实现一个 自动维护版本发布文档的 gradle 脚本,其中输入输出相关的代码以下所示:

task writeTask {
  inputs.property('versionCode'this.versionCode)
  inputs.property('versionName'this.versionName)
  inputs.property('versionInfo'this.versionInfo)
  // 一、指定输出文件为 destFile
  outputs.file this.destFile
  doLast {
    //将输入的内容写入到输出文件中去
    def data = inputs.getProperties()
    File file = outputs.getFiles().getSingleFile()
    
    // 写入版本信息到 XML 文件
    ...
    
}

task readTask {
  // 二、指定输入文件为上一个 task(writeTask) 的输出文件 destFile
  inputs.file this.destFile
  doLast {
    //读取输入文件的内容并显示
    def file = inputs.files.singleFile
    println file.text
  }
}

task outputwithinputTask {
  // 三、先执行写入,再执行读取
  dependsOn writeTask, readTask
  doLast {
    println '输入输出任务结束'
  }
}
复制代码

首先,咱们定义了一个 WirteTask,而后,在注释1处,指定了输出文件为 destFile, 并写入版本信息到 XML 文件。接着,定义了一个 readTask,并在注释2处,指定输入文件为上一个 task(即 writeTask) 的输出文件。最后,在注释3处,使用 dependsOn 将这两个 task 关联起来,此时输入与输出的顺序是会先执行写入,再执行读取。这样,一个输入输出的实际案例就实现了。若是想要查看完整的实现代码,请查看 Awesome-WanAndroid 的 releaseinfo.gradle 脚本

此外,在 McImage 中就利用了 dependsOn 的方式将自身的 task 插入到了 Gradle 的构建流程之中,关键代码以下所示:

// inject task
(project.tasks.findByName(chmodTask.name) as Task).dependsOn(mergeResourcesTask.taskDependencies.getDependencies(mergeResourcesTask))
(project.tasks.findByName(mcPicTask.name) as Task).dependsOn(project.tasks.findByName(chmodTask.name) as Task)
mergeResourcesTask.dependsOn(project.tasks.findByName(mcPicTask.name))
复制代码

经过 API 指定依赖顺序

除了 dependsOn 的方式,咱们还能够在 task 闭包中经过 mustRunAfter 方法指定 task 的依赖顺序,须要注意的是,在最新的 gradle api 中,mustRunAfter 必须结合 dependsOn 强依赖进行配套使用,其示例代码以下所示:

// 经过 API 指定依赖顺序
task taskX {
    mustRunAfter "taskY"

    doFirst {
        println "this is taskX"
    }
}

task taskY {
    // 使用 mustRunAfter 指定依赖的(一至多个)前置 task
    // 也可使用 shouldRunAfter 的方式,可是是非强制的依赖
//    shouldRunAfter taskA
    doFirst {
        println "this is taskY"
    }
}

task taskZ(dependsOn: [taskX, taskY]) {
    mustRunAfter "taskY"
    doFirst {
        println "this is taskZ"
    }
}
复制代码

五、Task 类型

除了定义一个新的 task 以外,咱们也能够使用 type 属性来直接使用一个已有的 task 类型(不少文章都说的是继承一个已有的类,不是很准确),好比 Gradle 自带的 Copy、Delete、Sync task 等等。示例代码以下所示:

// 一、删除根目录下的 build 文件
task clean(type: Delete) {
    delete rootProject.buildDir
}
// 二、将 doc 复制到 build/target 目录下
task copyDocs(type: Copy) {
    from 'src/main/doc'
    into 'build/target/doc'
}
// 三、执行时会复制源文件到目标目录,而后从目标目录删除全部非复制文件
task syncFile(type:Sync) {
    from 'src/main/doc'
    into 'build/target/doc'
}
复制代码

六、挂接到构建生命周期

咱们可使用 gradle 提供的一系列生命周期 API 去挂接咱们本身的 task 到构建生命周期之中,好比使用 afterEvaluate 方法 将咱们第三小节定义的 writeTask 挂接到 gradle 配置完全部的 task 以后的时刻,示例代码以下所示:

// 在配置阶段执行完以后执行 writeTask
this.project.afterEvaluate { project ->
  def buildTask = project.tasks.findByName("build")
  doLast {
    buildTask.doLast {
      writeTask.execute()
    }
  }
}
复制代码

须要注意的是,配置完成以后,咱们须要在 app moudle 下引入咱们定义的 releaseinfo 脚本,引入方式以下:

apply from: this.project.file("releaseinfo.gradle")
复制代码

5、SourceSet

SourceSet 主要是 用来设置咱们项目中源码或资源的位置的,目前它最多见的两个使用案例就是以下 两类

  • 1)、 修改 so 库存放位置
  • 2)、 资源文件分包存放

一、修改 so 库存放位置

咱们仅需在 app moudle 下的 android 闭包下配置以下代码便可修改 so 库存放位置:

android {
    ...
    sourceSets {
        main {
            // 修改 so 库存放位置
            jniLibs.srcDirs = ["libs"]
        }
    }
}
复制代码

二、资源文件分包存放

一样,在 app moudle 下的 android 闭包下配置以下代码便可将资源文件进行分包存放:

android {
    sourceSets {
        main {
            res.srcDirs = ["src/main/res",
                           "src/main/res-play",
                           "src/main/res-shop"
                            ... 
                           ]
        }
    }
}
复制代码

此外,咱们也可使用以下代码 将 sourceSets 在 android 闭包的外部进行定义

this.android.sourceSets {
    ...
}
复制代码

6、Gradle 命令

Gradle 的命令有不少,可是咱们一般只会使用以下两种类型的命令:

  • 1)、 获取构建信息的命令
  • 2)、 执行 task 的命令

一、获取构建信息的命令

// 一、按自顶向下的结构列出子项目的名称列表
./gradlew projects
// 二、分类列出项目中全部的任务
./gradlew tasks
// 三、列出项目的依赖列表
./gradlew dependencies
复制代码

二、执行 task 的命令

常规的用于执行 task 的命令有 四种,以下所示:

// 一、用于执行多个 task 任务
./gradlew JsonChao Gradle_Last
// 二、使用 -x 排除单个 task 任务
./gradlew -x JsonChao
// 三、使用 -continue 能够在构建失败后继续执行下面的构建命令
./gradlew -continue JsonChao
// 四、建议使用简化的 task name 去执行 task,下面的命令用于执行 
// Gradle_Last 这个 task
./gradlew G_Last
复制代码

而对于子目录下定义的 task,咱们一般会使用以下的命令来执行它:

// 一、使用 -b 执行 app 目录下定义的 task
./gradlew -b app/build.gradle MyTask
// 二、在大型项目中咱们通常使用更加智能的 -p 来替代 -b
./gradlew -p app MyTask
复制代码

7、总结

至此,咱们就将 Gradle 的核心 API 部分讲解完毕了,这里咱们再来回顾一下本文的要点,以下所示:

  • 1、Gradle 优点
    • 一、更好的灵活性
    • 二、更细的粒度
    • 三、更好的扩展性
    • 四、更强的兼容性
  • 2、Gradle 构建生命周期
    • 一、初始化阶段
    • 二、配置阶段
    • 三、执行阶段
    • 四、Hook Gradle 各个生命周期节点
    • 五、获取构建各个阶段、任务的耗时状况
  • 3、Project
    • 一、Project 核心 API 分解
    • 二、Project API
    • 三、project 属性
    • 四、文件相关 API
    • 五、其它 API
  • 4、Task
    • 一、从一个例子 🌰 出发
    • 二、Task 的定义及配置
    • 三、Task 的执行详解
    • 四、Task 的依赖和执行顺序
    • 五、Task 类型
    • 六、挂接到构建生命周期
  • 5、SourceSet
    • 一、修改 so 库存放位置
    • 二、资源文件分包存放
  • 6、Gradle 命令
    • 一、获取构建信息的命令
    • 二、执行 task 的命令

Gradle 的核心 API 很是重要,这对咱们高效实现一个 Gradle 插件无疑是必不可少的。由于 只有扎实基础才能走的更远,愿咱们能一同前行

参考连接:


Contanct Me

● 微信:

欢迎关注个人微信:bcce5360

● 微信群:

因为微信群已超过 200 人,麻烦你们想进微信群的朋友们,加我微信拉你进群。

● QQ群:

2千人QQ群,Awesome-Android学习交流群,QQ群号:959936182, 欢迎你们加入~

About me

很感谢您阅读这篇文章,但愿您能将它分享给您的朋友或技术群,这对我意义重大。

但愿咱们能成为朋友,在 Github掘金上一块儿分享知识。

相关文章
相关标签/搜索