Gradle 爬坑指南 -- Gradle 核心模型、Hook 函数、ext 扩展属性、Project API

5,965 阅读3分钟

能力有限,难免有误,请谅解!

书接上文:

上文我们了解了 Plugin 插件、Task 任务、Gradle 3大构建阶段。本文会继续深入,深入源码带大家了解 Gradle 3大内置对象:Gradle、Setting、Project,然后再了解下 Gradle 给我们提供的 hook 勾子函数

Gradle 核心模型

Gradel 核心模型是什么,很多人应该不清楚这个,其实就上前面有提到过的 Gradle 3大内置对象:Gradle、Setting、Project,看官方文档中的原话:

我怎么知道 setting script 背后对应的是哪个对象呢? 整个 Gradle 只有三个这样的对象: init script 对应 Gradle, setting script 对应 Settings, build script 对应 Project. 对这三者的关系, 你需要通过 Gradle 生命周期 来解惑

Gradle 构建工具虽然让我们使用 .gradle 脚本来写构建逻辑,但是在编译阶段还是会把脚本文件编程成 java 对象再执行

  • init.gradle 脚本编译完了会生成 Gradle 对象
  • settings.gradle 脚本编译完了会生成 Setting 对象
  • build.gradle 脚本编译完了会生成 Project 对象

Gradle 本质还是个 java 项目,推荐大家都把源码下下来,导入 AS 中看看,其实也不难,Gradle、Setting、Project 在源码中都是接口,实际我们获得到的对象都是实现类,这里我们只看接口定义的函数就能明白很多东西了,具体的实现类应该是引入的插件实现的,比如 android、java 构建插件

我们再来看看脚本文件中那些令我们头疼的 DSL 代码块,脚本中的 DSL 代码块除了来自插件的,剩下的都来自 Gradle 本身

app 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()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

脚本中每一个 DSL 都对应 Gradle 核心模型对象中的一个方法,buildscript{...}、repositories{...}、dependencies{...}、allprojects{...}、task{...} 这些都能找到对应的方法,大家且随我一起看看源码,不用多深入,单看接口设计就可以了

哈哈,看到这里,很多函数是不是很眼熟,是不是就是我们在 build.gradle 脚本中写的 DSL 代码块呀!遇到没见过的 DSL 大家不妨到 Gradle 项目源码中翻一翻,脚本里面能写什么 DSL 大家也可以过来看看源码,ctrl+F12 一下就清楚了,注释写的还可以,AS 有翻译插件,大家要善用翻译看官方的解释

查看源码的思路其实一开始我也没想到,没想到只要看看接口声明就能搞清楚状况,感谢来自 Gradle 团队的推广视频:来自Gradle开发团队的Gradle入门教程

官方出品,就是不一样,比博客、文档、其他机构的教学视频有深度多了,还没看的小伙伴强烈建议看一看。开眼界,有点体会到使用源码的味道了,不知道大家 get 到没有 ヾ(≧O≦)〃嗷~

Hook 函数

Gradle 的 Hook 函数也有叫生命周期的,就像是我们给 Application 注册监听函数一样,Gradle 允许我们在构建阶段施加自己的影响

这部分内容很繁杂,我也是看了很多文章,仔细推敲之后才决定从这个角度写这块内容

从不同角度对 Hook 函数进行分组

Gradle 提供的 Hook 函数挺多的,有点乱哈,反正我刚看完就是这感觉。这些 Hook 函数之间有不同的设计考虑、角度,基于此我们尝试对这些 Hook 函数进行分组,以方面记忆、理解

这里我们就要结合上面所讲的 Gradle 核心模型了 ヾ(´∀`o)+ 知识点之间从来都是相互关联的

1. 从 Gradle 核心模型的角度看:

  • Gradle、Settings 都是全局对象,其提供的 hook 函数自然对整个构建过程起作用
  • Project 对象针对的是单个项目,构建过程中会有多个 Project 对象,自然 Project 对象提供的函数只能对本项目起作用
  • Root Project 对象有些特殊,既有可以对全局进行设置的 hook 函数,也有只对自身起作用的 hook 函数,大家熟悉的 allProject() 就是其中可以对全局进行设置的函数。一般 hook 全局设置都不在根脚本中写,而是选择在 settings 脚本里写

2. 从函数命名的角度看:

  • Evaluated 单词代表构建阶段执行的单个 .gradle 脚本
    • beforeEvaluated 执行该脚本之前做什么
    • afterEvaluated 执行该脚本之后做什么
  • ProjectEvaluated 一个意思,但是 Project 比 Evaluated 含义大一点,Evaluated 专门是指每个 module 中的 .gradle 脚本。而 Project 是指某个 module 的整个构建过程或者可以理解为 Project 对象本身。在 hook 函数看上2者差不多,但还是要理解这2个单词的区别
    • beforeProject 执行该项目之前做什么
    • afterProject 执行该项目之后做什么

3. 从脚本执行的角度看:

  • 我们在 .gradle 脚本中写的 hook 函数,肯定只能在该脚本执行后才能生效,要是在脚本中定义诸如 beforeEvaluated 这样的函数大家觉得有意义吗?此时脚本都执行了,你再定义该脚本执行前应该执行什么就太晚了,应该想办法在该脚本执行前写这个函数

4. 从构建流程的角度看:

  • Initialization 初始化阶段 --> 会执行 init.gradle、settings.gradle 这2个脚本。init.gradle 脚本不推荐使用,因为影响范围太广,谁知道什么时候就会造成未知的困扰。我们都是在 settings.gradle 脚本中写 hook 函数
  • Configuration 编译阶段 --> 子项目脚本中一般只写影响范围只在本项目的 hook 函数。但是要注意 hook 函数执行的流程,比如你在子项目脚本中写 beforeEvaluated 就是纯扯淡。脚本都运行起来了,还写这个 hook 有个啥用。所以要注意 hook 必须写在正确的位置,要结合整个构建流程来考虑 hook 函数的书写位置
  • Execution 执行阶段 --> 这个阶段我们写不了 hook 函数的,就算是能写又能怎么样呢,该怎么执行构建任务,已经在上一个环节都已经决定好了,这个阶段我们施加不了任何影响

5. 从函数设计的角度看:hook 函数分2种

  • 一种是单个功能的 hook 函数,比如 beforeEvaluated,这类 hook 函数有很多
  • 一种是 listener 函数,这类函数往往可以同时实现对个功能,addLisenter() 就是这类 hook 函数,可以添加各种监听。大多数单功能的 hook 函数都可以用 listener 来代替。另外 listener 还可以通过遍历的方式实现对所有的 Project 进行设置、操作

6. 从函数作用的目标角度看:

  • 一种专门监听脚本的执行,比如 beforeEvaluated 就是
  • 一种专门监听项目的执行,比如 beforeProject,gradle.allProject() 可以对所有项目对象进行操作
  • 一种专门监听 task 的执行,比如 gradle.taskGraph.whenReady()

Hook 函数中 Gradle 3大对象初始化节点

因为整个构建过程很长,Gradle 3大对象不是一上来就都创建出来了,也是在构建过程中一步步才 new 出来的,在此之前我们就使用该对象是会报错的,所以我们必须明确 Gradle 3大对象 从哪个 hook 函数开始可以使用

换种说法 --> 我们虽然写的是脚本,但是实际代码还是要动态编译成 java 对象执行的,我们必须考虑在脚本中使用对象的时候,对象是不是已经初始化好了

1. Gradle 对象:

init.gradle 脚本执行过后,Gradle 就被创建出来了,所以我们在 settings.gradle 脚本里可以尽情使用 Gradle 对象。这也是 settings.gradle 脚本成为我们进行全局 hook 设置的原因

2. Setting 对象:

gradle.settingsEvaluated {
    .... settings 对象可以使用了
} 

settings.gradle 脚本执行过后,Setting 对象才算是可以让我们随便使用

3. Project 对象:

gradle.projectsLoaded {
    .... project 对象可以使用了
}

settings.gradle 脚本执行过程中会把所有子项目的 Project 对象创建出来,projectsLoaded() 这个函数中我们就能拿到所有所有 project 对象了

其实在每个脚本中,都可以使用该脚本对应的核心模型对象,脚本都跑起来了,对象就肯定已经创建出来了。我们要注意的是在脚本中使用超出本脚本范围的对象

Settings.gradle 脚本可以使用的 Hook 函数

方法注释和日志结合起来一起看

include ':libs'
include ':app'

buildscript {
	...
}

println("settings...")

// Setting 脚本执行前调用
gradle.beforeSettings {
    // 在这里写明显无用
    println("gradle.beforeSettings...")
}

// Setting 项目编译前调用
gradle.beforeProject {
    // 在这里写明显无用
    println("gradle.beforeProject...")
}

// Setting 脚本执行完成后调用
gradle.settingsEvaluated {
    println("gradle.settingsEvaluated...")
}

// Setting 项目编译完成后调用
gradle.afterProject {
    println("gradle.afterProject...")
}

// 所有项目脚本执行完后调用
gradle.projectsEvaluated {
    println("gradle.projectsEvaluated...")
}

// 开始进入编译阶段时调用
gradle.projectsLoaded {
    println("gradle.projectsLoaded...")
}

// 构建结束时调用
gradle.buildFinished {
    println("gradle.buildFinished...")
}

// 对所有项目脚本进行设置
gradle.allprojects(new Action<Project>() {
    @Override
    void execute(Project project) {
        // 在这里设置 beforeEvaluate 就能能作用了
        project.beforeEvaluate {
            println("gradle.allprojects.beforeEvaluate...")
        }
        project.afterEvaluate {
            println("gradle.allprojects.afterEvaluate...")
        }
    }
})

// 编译阶段 Task 流程图计算出来后调用
gradle.taskGraph.whenReady {
    println("gradle.taskGraph.whenReady...")
}

运行日志:

settings...
gradle.settingsEvaluated...
gradle.projectsLoaded...

> Configure project : --> 执行根目录脚本
gradle.beforeProject...
gradle.allprojects.beforeEvaluate...
root build.gradle...
gradle.afterProject...
gradle.allprojects.afterEvaluate...
root_project.afterEvaluate...

> Configure project :app --> 执行 app 壳工程脚本
gradle.beforeProject...
gradle.allprojects.beforeEvaluate...
app build.gradle...
gradle.afterProject...
gradle.allprojects.afterEvaluate...

> Configure project :libs --> 执行 libs 子项目脚本
gradle.beforeProject...
gradle.allprojects.beforeEvaluate...
libs build.gradle...
gradle.afterProject...
gradle.allprojects.afterEvaluate...
gradle.projectsEvaluated...
gradle.taskGraph.whenReady...

> Task :prepareKotlinBuildScriptModel UP-TO-DATE  --> 执行所有 Task 任务,这里省略了
gradle.buildFinished...

BUILD SUCCESSFUL in 2s

子项目 build.gradle 脚本可以使用的 Hook 函数

Settings.gradle 脚本能写的,这里都可以写。但是写在这里又有什么用呢,都开始跑具体的子项目构建脚本了,你再给全局添加 hook 不就晚了,就算设置了,也只能在该脚本之后脚本才能生效

能写的 hook 其实就是这2个了,日志输出看上面的就行

project.beforeEvaluate {
    // 写这里没用
    println("project.beforeEvaluate...")
}

project.afterEvaluate {
    println("root_project.afterEvaluate...")
}

Listener 类型的 hook 函数

Gradle Listener 监听也是推荐写在 Settings.gradle 脚本里,这个位置比较合适,root build.gradle 勉勉强强可以写,有几个 hook 函数会不起作用,因为其 hook 点都已经过去了

Gradle 添加 Listener 的方式很灵活,addListener() 函数接收的参数是 Object 对象,可以支持很多种类型的监听,详细看代码提示

1)addBuildListener()

函数可以代替一些全局设置的 hook 函数,里面的方法都是上面一些 hook 函数的重复

gradle.addBuildListener(new BuildListener() {
    @Override
    void buildStarted(Gradle gradle) {
        println("gradle.addBuildListener.buildStarted...")
    }

    @Override
    void settingsEvaluated(Settings settings) {
        println("gradle.addBuildListener.settingsEvaluated...")
    }

    @Override
    void projectsLoaded(Gradle gradle) {
        println("gradle.addBuildListener.projectsLoaded...")
    }

    @Override
    void projectsEvaluated(Gradle gradle) {
        println("gradle.addBuildListener.projectsEvaluated...")
    }

    @Override
    void buildFinished(BuildResult result) {
        println("gradle.addBuildListener.buildFinished...")
    }
} )

2) TaskExecutionGraphListener

可以监控所有 task 函数的执行

gradle.addListener(new TaskExecutionGraphListener() {
    @Override
    void graphPopulated(TaskExecutionGraph graph) {
        println("gradle.addListener.hasTask..."+graph.hasTask(":test"))
        graph.getAllTasks().each { 
            it.doLast {
                // ...
            }
            it.doFirst {
                // ...
            }
        }
    }
})

3) TaskExecutionListener

在 Task 执行前后添加钩子

gradle.addListener(object : TaskExecutionListener {
    override fun beforeExecute(task: Task) {
        ....
    }

    override fun afterExecute(task: Task, state: TaskState) {
        ....
    }
})

4) TaskActionListener

在 action 执行前后添加钩子

gradle.addListener(object : TaskActionListener {
    override fun beforeActions(task: Task) {
        ....
    }

    override fun afterActions(task: Task) {
        ....
    }
})

5) addTaskExecutionGraphListener

同 whenReady 的效果

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

6) DependencyResolutionListener

Gradle 会在所有子项目脚本执行后,进行依赖的决议。beforeResolve / afterResolve 方法就是 Gradle 为这项工作单独设置的一对 hook

gradle.addListener(object : DependencyResolutionListener {
    override fun beforeResolve(dependencies: ResolvableDependencies) {
        ....
    }

    override fun afterResolve(dependencies: ResolvableDependencies) {
        ....
    }
})

部分函数使用注意点

1. 判断是否包含指定 Task 要加:号

这个挺坑人的,一开始我都想到这里,发现总也找不到指定 Task,还有自定义 Task 要不在构建过程中不调用执行的话,Task 执行流程图里是找不到这个 Task 的

task test {
    println("task--test...")
    doLast {
        println("task--test.doLast...")
    }
}

gradle.addListener(new TaskExecutionGraphListener() {
    @Override
    void graphPopulated(TaskExecutionGraph graph) {
        println("gradle.addListener.hasTask..."+graph.hasTask(":test"))
        graph.getAllTasks().each { 
            it.doLast {
                // ...
            }
            it.doFirst {
                // ...
            }
        }
    }
})

所有 hook 流程图

图来自掘金小册,大体都在图里面,大家最后结合这张图再理解下

Hook 函数的意义

是给大家自定义 Task、Plugin 准备的,自定义的任务、插件中的任务你总得给他设置一个执行时机不是,所以 Hook 勾子就成了一个好的选择

Project API

Gradle 脚本中我们使用最多的,最让人头疼的就是 Project 对象的 API 了,感谢:深度探索 Gradle 自动化构建技术(三、Gradle 核心解密),我在这里整理一下

1. getAllprojects()

这个和 gradle.allProjects() 是一样的

// index = 0 是 Root buils.gradle 
project.getAllprojects().eachWithIndex { Project project, int index ->
    if (index == 0) println("index 0 :root project") else println("index $index : $project.name")
}

2. getSubprojects()

获取所有子项目

project.getSubprojects().eachWithIndex { Project project, int index ->
    println("index $index : $project.name")
}

3. getParent()

根项目对象获取到的是 null

project.getParent()

4. getRootProject()

project.getRootProject()

5. project()

通过 name 获取到指定子项目,当然 Gradle 对象也有 find 方法,但是这个写法我是第一次看见,有点意思

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

6. allprojects() / subprojects

这个不用说了,大家都熟悉,subprojects 是不会操作跟项目的

7. plugins

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

ext 扩展属性

ext{...} 这个 DSL 代码段也是 Project 对象提供的方法。ext{...} 大家都不陌生吧,都是用来做全局参数、依赖的配置。ext{...} 是 Gradle 提供的、让我们定义所需全局变量的代码块,简称:扩展属性

1. 定义 ext

一般我们在根项目脚本中写 ext{...}

// root build.gradle

ext {
    tag = "BB"
    age = 2
}

2. 使用 ext

大家注意这2种使用方式自动提示的环节,很多人抱怨没有代码提示好难用

// app build.gradle

// 方式1:直接使用,sync 之后出现自动提示
println( "name = $tag" )
println( "age = $age" )

// 方式1:借助 rootProject 对象,rebuild 之后出现自动提示
println( "name = ${rootProject.ext.tag}" )
println( "age = ${rootProject.ext.age}" )

3. 进阶使用 1 -- 抽象公共配置脚本

这个好理解,ext{...} 写在根目录脚本里有的人说应该拆分、专人专事,于是我们专门写一个 ext{...} 的脚本出来,该脚本一般都叫:config.gradle,然后 apple from 导入本目录脚本就能用了

ext{...} 编译过后是 Project 对象中的一个成员属性,ext{...} 中一般我们都是写 Map 来配置一些属性

// config.gradle

ext {

	// 不同的 DSL 配置块,推荐专门写一个 map 出来,这样方便查找
    android = [
            compileSdkVersion: 29,
            applicationId    : "com.bloodcrown.myapplication22",
            minSdkVersion    : 21,
            targetSdkVersion : 29,
            versionCode      : 1,
            versionName      : "1.0"
    ]
}
// root builf.gradle

apply from: this.file("config.gradle")

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"
    }
}
// app build.gradle

android {
    compileSdkVersion rootProject.ext.android.compileSdkVersion

    defaultConfig {
        applicationId rootProject.ext.android.applicationId
        minSdkVersion rootProject.ext.android.minSdkVersion
        targetSdkVersion rootProject.ext.android.targetSdkVersion
        versionCode rootProject.ext.android.versionCode
        versionName rootProject.ext.android.versionName
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
}    

4. 进阶使用 2 -- 抽象子项目脚本基类

这个也不难理解,我们的项目要是有多个 子项目 ,每个子项目中 重复的脚本配置 写起来也是让人讨厌的事,尤其是休休改改的情况下很讨厌的,我们应该延续 java 相面对象中的思路:一处修改,处处使用

这里我们需创建脚本基类,该脚本一般都叫:base_build.gradle,也是通过 apple from 导入子项目脚本就可以了

// base_build.gradle

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

android {
    compileSdkVersion rootProject.ext.android.compileSdkVersion

    defaultConfig {
        applicationId rootProject.ext.android.applicationId
        minSdkVersion rootProject.ext.android.minSdkVersion
        targetSdkVersion rootProject.ext.android.targetSdkVersion
        versionCode rootProject.ext.android.versionCode
        versionName rootProject.ext.android.versionName
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.3.2'
    implementation 'androidx.appcompat:appcompat:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
// app build.gradle

apply from: rootProject.file("base_build.gradle")

dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation 'com.google.android.material:material:1.0.0'
    implementation 'androidx.navigation:navigation-fragment-ktx:2.1.0'
    implementation 'androidx.navigation:navigation-ui-ktx:2.1.0'
}

图示:

5. 进阶使用 3 -- 使用遍历代替一行行手写

可能使有的项目依赖实在是太多了吧,于是出现了一种在 ext{...} 中写依赖,然后在脚本中遍历添加依赖的方式,方便是放便了,但是看着有些不习惯

ext{
    dependencies = [...]
    annotationd_ependencies = [...]
}

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

嘛~ 这种思路不是太能接受,大家看需求吧

6. ext{...} 中一样可以写代码

出了写一些配置参数,我们一些可以写代码的,看个例子:

ext {
  versionName = rootProject.ext.android.versionName
  versionCode = rootProject.ext.android.versionCode
  versionInfo = 'App的第2个版本,上线了一些最基础核心的功能.'
  destFile = file('releases.xml')
  
  if (destFile != null && !destFile.exists()) {
    destFile.createNewFile()
  }
}

this.project.afterEvaluate { project ->
  def buildTask = project.tasks.findByName("build")
  doLast {
    buildTask.doLast {
      writeTask.execute()
    }
  }
}

task writeTask {......}
(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))