能力有限,难免有误,请谅解!
书接上文:
上文我们了解了 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执行该脚本之后做什么
Project和Evaluated一个意思,但是 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))