Gradle 爬坑指南 -- 理解 Plugin、Task、构建流程

7,589 阅读13分钟

书接上文:

上文介绍了 Gradle、Groovy 的概念并且对其有了一些初步的认识,然后学习了 Groovy 基本语法,跑了第一个 Gradle 的 Hello World!此时想必大家对 Gradle 也算是有了一个清晰的初步认识。学习 Gradle 就是学习一门新的语言,然后应用其 API,恩就是这样,没什么太难的,即便有些不太理解的地方,但只要我们掌握了大体脉络,难点都是可以通过后面的学习掌握的,而不再像以前一样一说 Gradle 两眼一抹黑

怎么理解脚本

上文漏了一个概念:“脚本”,“.gradle” 就是一个脚本文件。我们说了 Groovy 是种基于 java 扩展出来的动态、脚本语言,脚本是什么我们淂清楚。其实大家经常听说脚本,但是很多人并不十分清楚脚本是什么,其实很简单

传统的 java 程序中,每一个 java 文件在开头我们都要声明 class,所有的代码都必须写在 Class{...} 中,要不编译器不认识。我们写出来的 class 叫类,类中还必须指明代码执行的起点,一般都是 main() 函数

class Person {
    int name
    int age
    
    public static void main(String[] args) {
       ...... 
    }
}

脚本 就是从 class 类这个概念中拓展出来的,一种写法更简单的文件:

  • 脚本文件不用在开头写 Class{...},可以直接写代码。当然脚本内还是可以写 class{...} 的,只不过不用在开头写了,当然写了 Groovy 也能认识
  • 以文件结尾 .xx 文件类型表示脚本文件类型,比如 .gradle 就表示这是一个 groovy 脚本,编译器根据这个表示去编译脚本文件
  • 脚本文件不用执行代码执行的开始位置或方法,脚本默认从第一行就开始执行代码
def to(x, y) {
    x + y
}

class Person {
    def name
    def age   
}

Person person1 = new Person()
Person person2 = new Person()
person1.name = "AA"
person2.name = "BB"

to(111,222)

脚本是一种更灵活、更简便的代码书写格式,实际还是要通过编译器编译成对应的类文件才能执行。比如 Groovy .gradle 脚本会编译成对应的 java 对象在 JVM 上执行。实际还要看对应的编译器,有的脚本文件对应的编译器可以不用编译成对应语言的对象而可以直接执行

Task 任务

官方文档 --> 配合 Google 浏览器翻译插件还是可以看的,推荐大家还是把官方文档过一遍,这个经历还是有必要的 o(^@^)o

先剧透一下

Task 是 Gradle 执行的基本单元,为什么这么说?实际上根据 Gradle 构建项目的流程,是先把所有的 .gradle 脚本执行一遍,编译生成对应的 Gradle、Setting、Project 对象,然后根据我们在构建脚本中设置的构建配置,生成一张 Task 组成的:有向无环图,先来看看这张图

最终,Gradle 会按照图上一个个 Task 的关联顺序挨个执行,每一个 Task 都完成一个特定功能,按照顺序把 Task 图执行一遍,整个项目的构建任务就完成了 ┗|`O′|┛ 嗷~~

什么是 Task

Gradle中,每一个待编译的工程都叫一个Project。每一个Project在构建的时候都包含一系列的Task。比如一个Android APK的编译可能包含:Java源码编译Task、资源编译Task、JNI编译Task、lint检查Task、打包生成APK的Task、签名Task等。一个Project到底包含多少个Task,其实是由编译脚本指定的插件决定。插件是什么呢?插件就是用来定义Task,并具体执行这些Task的东西

Task 我们可以看成一个个任务,这个任务可以是编译 java 代码、编译 C/C++ 代码、编译资源文件生成对应的 R 文件、打包生成 jar、aar 库文件、签名、混淆、图片压缩、打包生成 APK 文件、包/资源发布等。不同类型的项目 Task 任务种类不同,Gradle 现在可以构建 java、android、web、lib 项目

下图中这些都是一个个 Task 任务,每一个 Task 任务都是为了实现某个目的,可以理解为一个步奏,最终一个个步奏按序执行就完成了整个项目构建

Task 是完成一类任务,实际上对应的也是一个对象。而 Task 是由无数个 Action 组成的,Action 代表的是一个个函数、方法,每个 Task 都是一堆 Action 按序组成的执行图,就好像我们在 Class 的 main 函数中按照逻辑调用一系列方法一样

创建 Task

task hello{
    println "hello world"
}

task(hello2){
    println "hello world2"
}

task ('hello3'){
    println "hello world3"
}

运行 Task

  • 直接执行命令 gradle TaskName --> gradle hello
  • 执行某个子项目中的某个任务 gradle :MoudleName:TaskName --> gradle :app:clean

Task 的属性

这些属性都可以在创建 task 时写在 () 里

task test(name:"test",group:"AA"){
	dolast{
    	...
    }
}

Task 额外属性

task asss{
    ext {
        name = "AA"
        age = 18
    }

    doLast {
        println("name = "+ name)
        println("age = "+ age)
    }
}

Task 的 action

上面说过 Task 内部 action 也不是唯一的,而是一个集合,我们可以往里面添加各种 action,要注意 action 之间有前后执行顺序的,这个是规定好了的。这些 action 接收都是闭包,看下面 Task 中的声明

 //在 Action 队列头部添加 Action
 Task doFirst(Action<? super Task> action);
 Task doFirst(Closure action);

 //在 Action 队列尾部添加 Action
 Task doLast(Action<? super Task> action);
 Task doLast(Closure action);

 //删除所有的 Action
 Task deleteAllActions();

doFirst{...}、doLast{...} 接受的都是闭包

  • doFirst 添加的 action 最先执行
  • task 自身的 action 在中间执行,这个无法在 task 外部添加,可以在自定义 task 时写
  • doLast 添加的 action 最后执行

另外 task 也可以向对象那样操作,task 里面也可以写代码,比如打印 AA,但是这些代码只能在配置阶段执行,而 task 的 action 都是在运行阶段执行的。配置阶段和执行阶段不了解的看下面内容

task speak{
    println("This is AA!")
    doFirst {
        println("This is doFirst!")
    }
    doLast {
        println("This is doLast!")
    }
}

speak.doFirst {
	println("This is doFirst!")
 }

action 可以添加多个的,按照添加顺序执行,比如 doLast 就可以添加多个进来

task speak{
    println("This is AA!")
    doFirst {
        println("This is doFirst!")
    }
    doLast {
        println("This is doLast1...!")
    }
}

speak.configure {
    doLast {
        println 'his is doLast2...'
    }
}
speak.doLast{
        println 'his is doLast3...'
}

<< 简写

task task1 << {
    println "我是task1----"
}

<< 替代的是 doLast{...},这东西简单是简单了,但是阅读性全无,最讨厌这种东西了,实际增添很多困扰 ~

动态创建 Task

4.times { counter ->
    task "task$counter" {
        doLast {
            println "I'm task number $counter"
        }
    }
}

这样设置 task 任务执行顺序也是可以的 
task0.dependsOn task2, task3

Task 之间共享数据

2个 Task 之间使用、操作同一个数据。早先在 .gradle 脚本中直接声明变量,Task 直接使用该变量的方式是不行的、会报错,需要借助 ext 全局变量操行。但在 6.6.1 我发现可以直接用变量了,应该是官方优化了

ext {
    name = "AAA"
}

def age = 18

task s1 {
    doLast {
        age = 12
        rootProject.ext.name = "BBB"
        println("This is s1...")
    }
}

task s2 {
    doLast {
        println("age --> " + age)
        println("name --> " + rootProject.ext.name)
        println("This is s2...")
    }
}

Task 依赖

Task 依赖是指我们可以指定 Task 之间的执行顺序,重点理解 dependsOn 就行了 -->

  • A.dependsOn B --> 执行A的时候先执行B
  • A.mustRunAfter B --> 同时执行 A/B,先执行B在执行A,若执行关系不成立报错
  • A.shouldRunAfter B --> 同 mustRunAfter,但是执行关系不成立不会报错

1. dependsOn -->

task s1{
    doLast {
        println("This is s1...")
    }
}

task s2{
    doLast {
        println("This is s2...")
    }
}

s1.dependsOn s2

-----------或者----------------

task s2{
    dependsOn s1
    doLast {
        println("This is s2...")
    }
}

-----------dependsOn 还没声明出来的 task 要加 ""----------------

task s1{
    dependsOn "s2"
    doLast {
        println("This is s2...")
    }
}

2. mustRunAfter -->

task s1{
    doLast {
        println("This is s1...")
    }
}

task s2{
    doLast {
        println("This is s2...")
    }
}

s1.mustRunAfter s2

自定义 Task

Gradle 中 Task 都是继承自 DefaultTask,我们自定义 Task 也需要继承这个类,重点是写自己需要的方法,然后加上@TaskAction注解表示这个方法是 Task 中的 action,可以加多个,按照倒序执行

class MyTask extends DefaultTask {

    String message = "mytask..."

    @TaskAction
    def ss1() {
        println("This is MyTask --> action1!")
    }

    @TaskAction
    def ss2() {
        println("This is MyTask --> action2!")
    }

}

task speak(type: MyTask) {
    println("This is AA!")
    doFirst {
        println("This is doFirst!")
    }
    doLast {
        println("This is doLast!")
    }
}

系统默认 Task

gradle 默认提供了很多 task 给我们使用,比如 copy、delete

1. copy

copy 复制文件

task speak (type: Copy) {
    ...
}
//数据源目录,多个目录
public AbstractCopyTask from(Object... sourcePaths)  

//目标目录,单一
public AbstractCopyTask into(Object destDir) 

//过滤文件 包含
public AbstractCopyTask include(String... includes)

//过滤文件 排除
public AbstractCopyTask exclude(String... excludes)

//重新命名,老名字 新名字
public AbstractCopyTask rename(String sourceRegEx, String replaceWith)

//删除文件 Project 接口
boolean delete(Object... paths);
复制图片:多个数据源 -->

task copyImage(type: Copy) {
    from 'C:\\Users\\yiba_zyj\\Desktop\\gradle\\copy' , 
         'C:\\Users\\yiba_zyj\\Desktop\\gradle\\copy'
    into 'C:\\Users\\yiba_zyj\\Desktop'
}
复制文件:过滤文件,重命名 -->

task copyImage(type: Copy) {
    from 'C:\\Users\\yiba_zyj\\Desktop\\gradle\\copy'
    into 'C:\\Users\\yiba_zyj\\Desktop'
    include "*.jpg"
    exclude "image1.jpg"
    rename("image2.jpg","123.jpg")
}

2. Delete

删除文件

删除桌面上的文件 -->

task deleteFile(type: Delete) {
    //删除系统桌面 delete 
    delete "C:\\Users\\yiba_zyj\\Desktop\\gradle\\delete"
}

设置默认 Task 任务

设置这个的意思的是指,脚本中我们不调用该 task,设置的 task 也会执行

defaultTasks 'clean', 'run'

task clean {
    doLast {
        println 'Default Cleaning!'
    }
}

task run {
    doLast {
        println 'Default Running!'
    }
}

task other {
    doLast {
        println "I'm not a default task!"
    }
}
> gradle -q

Default Cleaning!
Default Running!

Task 中使用外部依赖

代码来自官网,buildscript{...} 引入远程仓库和依赖 path,import 导入进来就可以用了

import org.apache.commons.codec.binary.Base64

buildscript {
	// 导入仓库
    repositories {
        mavenCentral()
    }
    // 添加具体依赖
    dependencies {
        classpath group: 'commons-codec', name: 'commons-codec', version: '1.2'
    }
}

task encode {
    doLast {
        def byte[] encodedString = new Base64().encode('hello world\n'.getBytes())
        println new String(encodedString)
    }
}

Plugin 插件

可能解释的不是非常恰当,但是基本就是这个意思啦

首先记住插件这个单词:Plugin

理解什么是插件

我们写项目要使用大量的第三方代码,Gradle 作为构建工具,自然要拥有管理第三方代码的能力,要不怎么打包生成最终输出物。因为这些额外的代码要打入最终的生成物中,所以管理第三方代码的功能 Gradle 是必须要有的

这些第三方代码有些是参与业务代码的,有些则是参与项目构建的:

  • 参与项目构建的第三方代码叫 --> 插件
  • 参与代码逻辑的第三方代码叫 --> 依赖

不管是插件,还是依赖,本质都是一堆类、函数,就是 API,区别是使用的地方不同

Gradle是一个框架,作为框架,它负责定义流程和规则。而具体的编译工作则是通过插件的方式来完成的。比如编译Java有Java插件,编译Groovy有Groovy插件,编译Android APP有Android APP插件,编译Android Library有Android Library插件。Gradle中每一个待编译的工程都是一个Project,一个具体的编译过程是由一个一个的Task来定义和执行的。一个Project到底包含多少个Task,其实是由编译脚本指定的插件决定。插件是什么呢?插件就是用来定义Task,并具体执行这些Task的东西

插件的层级关系

Gradle 插件的整体架构是这样的

  • 最底层的是 Gradle 框架,提供一些基础服务,如 task 的依赖,有向无环图的构建等通用任务,规定构建规范
  • 中间的是 Google 团队开发的 Android Gradle plugin 插件,提供了很多与 Android 项目打包有关的 task 以及 变体 的输出
  • 最下面的是 开发者自定义的 Plugin,可以 hook 官方 task 在其上加上自己的任务、使用 Transform 进行编译时代码注入

插件的作用:

Gradle 本身是一个通用的构建系统, 它并不知道你要编译的项目或代码是 Java 还是 C。Java 代码需要 javac 把 .java 编译为 .class,而 C 代码需要 gcc 把 .c 编译为 .o

在介绍 Task 的部分我们说过整个构建过程就是由一个个 Task 任务构成的,一个项目的构建包括很多步奏:代码编译、资源编译、依赖库打包等操作。基于提取公共、抽象特异的模板思路,Gradle 作为通用项目构建工具,Gradle 封装的是项目构建的公共部分,而针对每种项目的特点和不同,就由每种项目对应的构建插件来接过差异部分的构建

这些差异要是让 coder 我们自己来做,谁也记不住这么多不同不是,谁没事去背这个,因此插件就诞生了。apply plugin: 'com.android.application' 是 Android 项目的构建插件,apply plugin: 'com.android.library' 是 Android 依赖项目的构建插件,这些插件封装了对应项目的整个构建流程,具体就是说这些插件内部已经定义好了一个 Task 有向无环图

我们只需要在 .gradle 构建脚本中引入插件, 根据项目情况配置几个属性, 即可实现项目的构建,这就是 Java 插件的目的

当然为了完成某个功能,我们也可以把这些代码写成插件存入远程仓库中供大家使用~

插件命名规则

插件一般都存在远程仓库中,我们要从仓库中找到指定插件需要遵循固定的规则好查找这些插件

  1. 通用仓库规则 --> group:name:version

    • 例:com.walfud:myplugin:1.2.3
    • maven、google、jcenter、jvy 这些仓库,都是通过这种名称规则引用插件的
  2. gradle 仓库规则 --> id:id.gradle.plugin:version

    • 例:com.android.application:com.android.application.gradle.plugin:1.2.3
    • gradle 仓库非淂和别人不一样,看着蛋疼,真是没有困难也得创造困难 ヾ(≧O≦)〃嗷~
  3. Gradle 切换 Google 仓库(这个不一定对,掘金小册这么写的)

    • Gradle 仓库:com.android.application:com.android.application.gradle.plugin:1.2.3
    • Google 仓库:com.android.tools.build:gradle:1.2.3
pluginManagement {
    repositories {
        ....
        google()
    }

    resolutionStrategy {
        eachPlugin {
            if (requested.id.namespace == "com.android") {
                useModule("com.android.tools.build:gradle:${requested.version}")
            }
        }
    }
}

这是官方文档上写的,但是我试了报错,也许是改了,我这 Gradle 版本:6.6.1

最后是 Gradle 仓库对应插件命名的介绍(说真的,我真不知道这个有啥用 ヾ(´∀`o)+ )

Gradle 内置插件

Gradle 内置了很多插件, 比如 java 插件, build-scan 插件, 引入这种插件我们直接通过 Gradle 内置函数就可以, 不用指定 id 和 version

比如这样的就是内置插件:

repositories {
	google()
	jcenter()
}

google()、jcenter() 明显就是一个函数嘛 (○` 3′○) 我们有看到插件的 group:name:version 了吗,明显没有呀,那为什么我们还能引入这个插件呢?

答案就是 Gradle 已经帮我们写好这块啦 (>▽<) 我们点击 google()、jcenter() 会链接到一个类:RepositoryHandler。内置的意思就是 Gradle 已经写好啦,我们拿来直接用就好了

gradle 内置插件可以看到都是插件仓库,当然仓库本身也是一种插件就是了,理解起来有点绕

导入插件

插入插件,我们先要导入仓库,也就声明从哪些仓库查找插件和远程依赖,然后再导入插件。只要我们在根项目的 build.gradle 构建脚本中声明导入的插件,那么所有子项目就都可以使用该插件了

  • 根项目 build.gradle 导入插件
buildscript {
    repositories {
        google()   
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.0'
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}
  • 子项目使用插件
app build.gradle --> 

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    ....

    defaultConfig {
        ....
    }
    buildTypes {
        ....
    }
}

dependencies {
    ....
}

其中几个 {...} 闭包解释下:

  • buildscript {...} --> 声明仓库、添加插件专用闭包
  • repositories {...} --> 设置远程仓库
  • dependencies {...} --> 添加插件地址,当然也可以添加在 setting.gradle 脚本中使用的远程依赖
  • apply --> 使用插件,插件虽然导入进来了,但是子项目用不用就是个事了,要是不用的话,是不需要打包进来的,所以要主动声明下

apple 就是查找插件中的 apple 方法并调用他就是这么简单,gradle 会帮你实例化这么个类,并调用他的方法

repositories {...} 这个闭包是还有其他参数,比如远程仓库的账号、密码等

repositories {
    maven {
        url = uri(...)
        
        username = "joe"
        password = "secret"
    }
}

dependencies {...} 这里主要写的是使用到的插件地址,当然出了插件也可以写在 setting.gradle 脚本中使用的远程依赖。典型的比如 butterknife 这个库,这个库就是要以插件的形式使用,不在 setting.gradle 脚本的 dependencies{...} 写插件地址的话,后面就没法用 apply 使用插件

buildscript {
  repositories {
    mavenCentral()
    google()
  }
  dependencies {
    classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'
  }
}

apply plugin: 'com.android.library'
apply plugin: 'com.jakewharton.butterknife'

配置插件属性

这个是重点,务必要理解 (○` 3′○)

前文说过插件是参与项目构建的第三方代码集合,这些代码不光会参与整个项目构建,同样也需要用户参入一些配置参数,参数配置也是用闭包的方式传入

插件内部会有对应的方法,接收这些在 build.gradle 脚本中配置的参数。给插件传递配置参数的闭包叫 DSL 代码段,最经典的场景就是 android {...} 了

app build.gradle -->

apply plugin: 'com.android.application'

android {
    compileSdkVersion(28)

    defaultConfig {
        ....
    }
}

android {...} 这个 DSL 可不是 Gradle 自身的配置,而是来源于 com.android.application 这个插件。大家只要明白这个后面再看 build.gradle 就明白多了,大家把 DSL 当做插件的 init 方法好了,本质上就是这么回事

插件中使用 delegate + 闭包实现 DSL 配置块

这个在语法阶段已经说过了,这里再贴一遍,就是为了强调这个的重要性

其实思路很简单,每一个 {...} 闭包都要有一个对应的数据 Bean 存储数据,在合适的时机 .delegate 即可

  1. 闭包定义
def android = {
	compileSdkVersion 25
	buildToolsVersion "25.0.2"
    
    // 这个对应相应的方法
	defaultConfig {
		minSdkVersion 15
		targetSdkVersion 25
		versionCode 1
		versionName "1.0"
	}
}
  1. 准备数据 Bean
class Android {
    int mCompileSdkVersion
    String mBuildToolsVersion
    BefaultConfig mBefaultConfig

    Android() {
        this.mBefaultConfig = new BefaultConfig()
    }

    void defaultConfig(Closure closure) {
        closure.setDelegate(mProductFlavor)
        closure.setResolveStrategy(Closure.DELEGATE_FIRST)
        closure.call()
    }
}

class BefaultConfig {
    int mVersionCode
    String mVersionName
    int mMinSdkVersion
    int mTargetSdkVersion
}
  1. .delegate 绑定数据
Android bean = new Android()
android.delegate = bean
android.call()

Gradle 构建过程

下面我说:Gradle 脚本的执行 --> 是指 gradle 编译那些 .gradle 脚本文件,生成对应的对象和 Task 任务

Gradle 的构建过程是通用的,任何由 Gradle 构建的项目都遵循这个过程

Gradle 构建分为三个阶段,每个阶段都有自己的职责,每个阶段都完成一部分任务,前一阶段的成果是下一阶段继续执行的前提:

  • Initialization --> 初始化阶段。按顺序执行 init.gradle -> settings.gradle 脚本,生成 Gradle、Setting、Project 对象
  • Configuration --> 编译阶段,也叫配置阶段。按顺序执行 root build.gradle -> 子项目 build.gradle 脚本,生成 Task 执行流程图
  • Execution --> 执行阶段。按照 Task 执行图顺序运行每一个 Task,完成一个个步奏,生成最终 APK 文件

看图:

整个构建过程,官方在一些节点都设置有 hook 钩子函数,以便我们在构建阶段添加自己的逻辑进来,影响构建过程。hook 钩子函数可以理解为监听函数,另外也可以正儿八经的设置 Linsener 监听函数

接下来我会重点介绍一下 init.gradlesettings.gradle 这2个脚本

Initialization 阶段

Initialization 是初始化阶段,一上来会马上把全局的 Gradle 内置对象 new 出来,Gradle 对象中的参数都是本次 gradle 构建的全局属性,通过 Gradle 对象可以干很多事

  • 比如可以获取 Gradle Home、Gradle User Home 目录
  • 添加全局监听
  • 给所有项目添加设置、依赖等

Initialization 阶段会按照先后顺序运行2个 Groovy 脚本:

  • Init Script: 创建内置对象 Gradle
  • Setting Script: 创建内置对象 Setting、每个 module 对应的 Project 对象

Configuration 阶段

Initialization 阶段后就是 Configuration 阶段了,Configuration 阶段会执行所有的 build.gradle 脚本,先执行根目录即根项目的构建脚本,再根据子项目之间的依赖关系,挨个执行子项目的构建脚本

Gradle 中有根项目和子项目之分,不管是什么项目在 Gradle 中都对应一个 Project 对象。根项目只有一个,而子项目可以有多个,android 中即便是 app module 我们经常说的壳工程,其实也是一个子项目。Gradle 默认的根项目是空的,没有内容的,跟项目只有 .gradle 有实际意义,其他都没用,不要在意

Gradle 中每一个项目都必须有一个 .gradle 构建脚本,Configuration 阶段会执行所有项目的构建脚本,从跟项目开始一直到把所有参与本次构建的子项目构建脚本都执行完,在这个过程中,会根据 .gradle 构建脚本内容,创建对应的 Project 对象,在后面的环节,1个 Project 对象就对应着1个项目啦,rootProject 表示根项目,Project 表示子项目

.gradle 脚本里的 DSL 配置块都是 Project 对象的方法而已

然后根据脚本中的配置,生成 Configuration 阶段的最终产物:Task 有向无环图,给下一阶段执行。Configuration 阶段的目的就是根据脚本配置计算出整个构建过程需要的逻辑和流程,可以理解为动态生成代码的过程。有了动态生成的代码,后面才好有东西执行不是 (^-^

Execution 阶段

Execution 阶段没啥说的了,就是拿到 Configuration 阶段计算出来的 task 执行图,按照顺序一个个跑这些 task 就能完后构建了

可以通过 Gradle.getTaskGraph() 方法来得到该有向无环图

当有向无环图构建完成之后,可以通过 whenReady 或者 addTaskExecutionGraphListener(TaskExecutionGraphListener) 来接收相应的通知

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

这是 Android 构建流程,其中每一个环节都可以认为是一个 Task(实际每个环节都有多个 Task)

大家仔细体会上面这张图,不难的,挨个环节看,整个构建流程就是由这一个个细分的步奏组成的,至于有哪些步奏,哪些先开始,哪些最后执行,这就要看你在项目中导入的是什么插件了。前文说过,Gradle 是通用构建工具,用于制定规则,详细的每种项目该怎么构建。过程是什么样的,插件负责的就是这些具体的内容、流程。大家再体会下插件的含义 o( ̄ε ̄*)

下面打印一下 rebuild 的过程,大家体会下什么叫构建过程是由 task 组成的,task 即是一个个细分的步奏

Executing tasks: [clean, :libs:assembleDebug, :app:assembleDebug] in project /Users/zbzbgo/worksaplce/flutter_app4/MyApplication

Starting Gradle Daemon...
Gradle Daemon started in 1 s 423 ms

> Configure project :
This is AA!
index == class java.lang.Integer

> Task :clean
> Task :libs:clean
> Task :libs:preBuild UP-TO-DATE
> Task :libs:preDebugBuild UP-TO-DATE
> Task :libs:compileDebugAidl NO-SOURCE
> Task :app:clean
> Task :app:preBuild UP-TO-DATE
> Task :app:preDebugBuild UP-TO-DATE
> Task :app:compileDebugAidl NO-SOURCE
> Task :libs:mergeDebugJniLibFolders
> Task :libs:compileDebugRenderscript NO-SOURCE
> Task :app:createDebugCompatibleScreenManifests
> Task :app:extractDeepLinksDebug
> Task :app:mergeDebugShaders
> Task :app:javaPreCompileDebug
> Task :app:compileDebugShaders NO-SOURCE
> Task :app:generateDebugAssets UP-TO-DATE
> Task :app:processDebugJavaRes NO-SOURCE
> Task :libs:packageDebugResources
> Task :libs:parseDebugLocalResources
> Task :libs:mergeDebugNativeLibs
> Task :libs:stripDebugDebugSymbols NO-SOURCE
> Task :libs:copyDebugJniLibsProjectAndLocalJars
> Task :libs:mergeDebugShaders
> Task :libs:compileDebugShaders NO-SOURCE
> Task :libs:packageDebugAssets
> Task :libs:packageDebugRenderscript NO-SOURCE
> Task :app:generateDebugResources
> Task :libs:compileDebugLibraryResources
> Task :libs:copyDebugJniLibsProjectOnly
> Task :libs:processDebugManifest
> Task :libs:javaPreCompileDebug
> Task :libs:compileDebugKotlin
> Task :app:processDebugManifest
> Task :app:mergeDebugAssets
> Task :libs:compileDebugJavaWithJavac
> Task :libs:compileDebugSources
> Task :libs:assembleDebug
> Task :app:mergeDebugJniLibFolders
> Task :app:mergeDebugResources
> Task :app:mergeLibDexDebug
> Task :app:processDebugResources
> Task :app:mergeExtDexDebug
> Task :app:compileDebugKotlin
> Task :app:compileDebugJavaWithJavac
> Task :app:compileDebugSources
> Task :app:mergeDebugNativeLibs
> Task :app:dexBuilderDebug
> Task :app:mergeProjectDexDebug
> Task :app:mergeDebugJavaResource
> Task :app:packageDebug
> Task :app:assembleDebug

Deprecated Gradle features were used in this build, making it incompatible with Gradle 7.0.
Use '--warning-mode all' to show the individual deprecation warnings.
See https://docs.gradle.org/6.6.1/userguide/command_line_interface.html#sec:command_line_warnings

BUILD SUCCESSFUL in 1m 6s
51 actionable tasks: 49 executed, 2 up-to-date

上面截取了部分,太多了不宜阅读,大家看个效果,可以自己尝试下在 build 控制台看下输出日志

图解构建过程

上面构建过程的内容都是来自官方文档的大路货,下面说点不一样的,资料来自于:

感谢前辈们的持续输出,下面比上面要详细一点,对于大家理解整个过程非常有帮助,仔细看这张图其实大家就能明白了

当我们开始执行 Gradle 构建命令时,要经历下面几个步奏:

  1. 首先,初始化 Gradle 构建框架自身
  2. 然后,把命令行参数包装好送给 DefaultGradleLauncher
  3. 最后,触发 DefaultGradleLauncher 中 Gradle 构建的生命周期,并开始执行标准的构建流程

前面学习过的朋友都知道 Gradle 有3个进程:Wrapper、Client、Deamon

  • Client 进程 负责管理每一次具体的构建任务,构建完毕 Client 进程关闭。期间会解析各种构建、初始化配置,创建出 Gradle 对象,然后把这些参数传递给 Deamon 进程,期间接受 Deamon 进程发回来的 Gradle 构建日志
  • Deamon 进程 负责具体的构建,Deamon 进程是守护进程,构建结束不会关闭进程,而是会缓存复用该进程
  • Wrapper 进程 负责管理 Gradle 版本

1) Wrapper 第一个先上

大家看图,构建开始后第一个执行的程序来自于:Wrapper,AS 开发工具必须首先确定 Gradle 环境 OK 才能启动构建

调用 gradle/wrapper/gradle-wrapper.jar/GradleWrapperMain.main() 启动 wrapper 任务

public static void main(String[] args) throws Exception {

        // 1、根据 gradle-wrapper.properties 中 gradle zip 地址,动态构建成一个 WrapperExecutor 实例
        WrapperExecutor wrapperExecutor = WrapperExecutor.forWrapperPropertiesFile(propertiesFile);
        // 2、执行 execute 方法开始任务
        wrapperExecutor.execute(args, new Install(logger, new Download(logger, "gradlew", "0"), new PathAssembler(gradleUserHome)), new BootstrapMainStarter());
}

public void execute(String[] args, Install install, BootstrapMainStarter bootstrapMainStarter) throws Exception {
        // 1、下载 gradle wrapper 依赖与源码
        File gradleHome = install.createDist(this.config);
        // 2、从这里开始执行 gradle 的构建流程
        bootstrapMainStarter.start(args, gradleHome);
    }

2) BootstrapMainStarter 对象控制整个构建过程

BootstrapMainStarter.start() 就正式开始 Gradle 构建过程了,会依次执行 BootstrapMainStarter 的4个方法:

  • getLoadedSettings
  • getConfiguredBuild
  • executeTasks
  • finishBuild

详细的不说了,大家看图就行了,这张图非常 Nice

BootstrapMainStarter 对象一旦开始执行,会先把 Client 进程初始化出来,然后再执行全局的初始化脚本 init.gradle,初始化全局 Gradle 对象。注意在执行 settings.gradle 脚本之前,会先解析 gradle.properties 中配置的全局参数

后面的就没什么好说的了,有兴趣的去看源码,没时间的看图就行了

3)最详细的 Android 插件打包流程图

init.gradle 脚本

init.gradle 初始化脚本作用于机器中所有 Gradle 项目,是每一次构建过程第一个执行的脚本。执行该脚本,会创建 Gradle 对象

因为 init.gradle 初始化脚本是作用于本机所有项目的,所以该脚本编写位置不在某个具体的项目中,而是在 Gradle 的配置环境中添加

init.gradle 脚本配置路径

请在以下路径中任意一处添加 init.build 文件:

  • GRADLE_USER_HOME/init.gradle
  • GRADLE_USER_HOME/init.d/init.gradle
  • 你要是以本地文件形式管理 Gradle,这里就你是本地 Gradle 的地址

在初始化脚本中打印一些数据看看

println("init...")
println("gradleHomeDir:${gradle.gradleHomeDir}")
println("gradleUserHomeDir:${gradle.gradleUserHomeDir}")
println("gradleVersion:${gradle.gradleVersion}")
println("startParameter:${gradle.startParameter}")

initscript {...}

该脚本中有一个 DSL 配置块:initscript {...},可以添加远程依赖,该配置块在脚本中第一个运行

观察 initscript {...} 运行顺序

println("init...")

initscript{
    println("initscript...")
}

initscript {...} 中导入第三方库并使用,以下来自官方文档

import org.apache.commons.math.fraction.Fraction

initscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.apache.commons:commons-math:2.0'
    }
}

println Fraction.ONE_FIFTH.multiply(2)

使用初始化脚本实现全局配置

initscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.apache.commons:commons-math:2.0'
    }
}

官方文档上说初始化脚本的作用就是做全局配置:

  • 设置企业范围的配置,例如在哪里可以找到自定义插件
  • 根据当前环境设置属性,例如开发人员的计算机与持续集成服务器
  • 提供构建所需的有关用户的个人信息,例如存储库或数据库身份验证凭据
  • 定义机器特定的详细信息,例如JDK的安装位置
  • 注册构建侦听器。希望监听Gradle事件的外部工具可能会发现这很有用
  • 注册构建记录器。您可能希望自定义Gradle如何记录其生成的事件

settings.gradle 脚本

初始化阶段,init.gradle 脚本执行过后,跟着会执行 settings.gradle 脚本。settings.gradle 脚本的作用就是管理子项目 Project 之间的依赖关系,决定本次构建有哪些 Project 参与

注意点:

  1. 一个 Gradle 项目只需要一个 settings.gradle 脚本就行了
  2. settings.gradle 脚本一旦执行,就会创建 Setting 对象
  3. 每一个参与打包的 module 都会生成对应的 Project 对象,此时 Project 对象的 build.gradle 脚本还没运行,Project 对象中的内容还是空的,但是不耽误我们在此时对其进行设置

include 函数

include 函数用于明确参加构建的子项目有哪些,凡是 include 的项目都会参与本次打包。默认使用相对路径,以跟项目路径为准,所以使用 include ':app' 即可,但若是我们对 module 项目的目录进行了修改,那么就得自己设置 file 路径了

// 通用设置
include ':app', ':baseComponents', ':baselib', ':commonRepositroy', ':player'

// 手动设置--方式1:相对路径
project(':baseComponents').projectDir = new File('components/baseComponents')
project(':baseComponents').projectDir = new File('../components/baseComponents')

// 手动设置--方式2:绝对路径
project(':baseComponents').projectDir = new File(rootProject.projectDir.path,'/components/baseComponents')

// 导入其他工程的 module 子项目
include "speech"
project(":speech").projectDir = new File("../OtherApp/speech")

项目路径如下图:

进行全局配置

init.gradle 脚本毕竟是对本机所有项目进行操作,范围有些广,有时候我们并不需要这个大的范围。要是只想对本项目进行设置的话,这些设置放到 settings.gradle 里其实也是没问题的。此时子项目的 build.gradle 构建脚本还没执行呢,所以我们可以为所欲为

这个其实就是把根项目 build.gradle 脚本中的 buildscript{...} 整体移到 setting.gradle 脚本里了。这里坑的地方在于网上博客和官方文档都没提 buildscript{...} 的事,让你直接写 repositories{...}

以前根项目的 build.gradle 脚本中一般会有 buildscript {...} DSL 配置块声明依赖的插件,这已经是过时的写法, Gradle 最新版本中将这个部分挪到 setting.gradle 中了

setting.gradle -->

include ':app'
rootProject.name = "My Application"

buildscript {
    ext.kotlin_version = "1.4.10"
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.0.1"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"   
    }
}

pluginManagement 好像不能用了

我这里只要写 pluginManagement 就编译就过不去,应该是有变动,但是我没在官方文档看到说明

pluginManagement {
    resolutionStrategy {
        eachPlugin {
            if (requested.id.id == "com.dorongold.task-tree") {
                useVersion("1.4")
            }
        }
    }
}

root build.gradle

子项目的 .gradle 构建脚本里面写的都是插件的 DSL 配置块,比如 android{...},这个下篇文章再说。本文说说根项目中的 .gradle 构建脚本

根项目中的构建脚本别的不干,实际上就干两件事:

  • 一个是引入插件
  • 另一个是配置全局属性

上面说 settings.gradle 脚本时提到了 Gradle 新版本已经把根项目脚本中的插件配置 DSL 移到 settings.gradle 里了,其实还放在根项目脚本里也没事,一样用

函数配置和 init.gradle 脚本一样,init 脚本看懂了,这里也一样写,代码其实都差不多

root build.gradle -->

ext.kotlin_version = "1.4.10"
buildscript {
    ext.kotlin_version = "1.4.10"
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.0.1"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

Project 之间的关系

无论在哪个 build script 中, 都可以通过 rootProject 获取到 root build script 所对应的对象
父模块可以通过 subprojects 获取到所有的子模块
子模块可以通过 parent 获取到父模块
如果两个模块之间没有直系关系, 则可以通过 findProject 引用.
默认情况下, 所有的模块是按照字母序进行 evaluate 的
如果某个模块依赖另一个模块, 则需要使用 evaluationDependsOn 指明依赖关系, 这样被依赖的模块就会优先被 evaluate

gradle.properties 文件

gradle.properties 一般我们用来写全局参数的,就像是 static 一样,Gradle 早期用的很广泛的。该文件中每一个参数,可以被任意位置的 .gradle 脚本引用,即便是子项目也是可以的。gradle.properties 文件的位置在项目有根目录下

此外 gradle.properties 文件中还可以配置 VM option 参数,如堆栈大小等

gradle.properties -->

org.gradle.jvmargs = -Xmx2g -XX:MaxMetaspaceSize = 512m -XX:+ HeapDumpOnOutOfMemoryError -Dfile.encoding = UTF-8

使用起来也很简单,直接调名字就行,注意起名字不要和 Gradle 系统内置参数重名,最好别重名

gradle.properties -->

nameAA = "AAA"
age = 123

app build.gradle -->

println("name:" + nameAA)
println("age:" + age)