前言
在上一篇文章中,对Gradle基础以及构建机制进行了详细的讲解,在这一篇中将会对Gradle核心模型以及Gradle插件进行讲解。
1.Gradle核心模型
1.1 Gradle钩子函数
讲钩子函数,还是得拿出Gradle执行流程图
如图所示
- gradle在生命周期三个阶段都设置了相应的钩子函数调用。
- 使用钩子函数,处理自定义的构建:
- 初始化阶段:gradle.settingsEvaluated和gradle.projectsLoaded。(在settings.gradle中生效)
- 配置阶段:project.beforeEvaluate和project.afterEvaluate;gradle.beforeProject、gradle.afterProject及gradle.taskGraph.taskGraph.whenReady。
- 执行阶段:gradle.taskGraph.beforeTask和gradle.taskGraph.afterTask。
而Gradle也可以监听各个阶段的回调处理:
- gradle.addProjectEvaluationListener
- gradle.addBuildListener
- gradle.addListener:TaskExecutionGraphListener (任务执行图监听),TaskExecutionListener(任务执行监听),TaskExecutionListener、TaskActionListener、StandardOutputListener ...
概念又说了一大堆,撸码验证一下!
- 打开AS,创建一个普通工程项目。
- 进入项目
build.gradle
(外层)文件 - 在末尾添加如下代码:
// =======================================
// Gradle提供的钩子函数
// 配置阶段:
gradle.beforeProject {
println "gradle.beforeProject"
}
gradle.afterProject {
println "gradle.afterProject"
}
gradle.taskGraph.whenReady {
println "gradle.taskGraph.whenReady"
}
beforeEvaluate {
println "beforeEvaluate"
}
afterEvaluate {
println "afterEvaluate"
}
//==================
// 为gradle设置监听
gradle.addProjectEvaluationListener(new ProjectEvaluationListener() {
@Override
void beforeEvaluate(Project project) {
println "Configure listener beforeEvaluate"
}
@Override
void afterEvaluate(Project project, ProjectState state) {
println "Configure listener afterEvaluate"
}
})
gradle.addBuildListener(new BuildListener() {
@Override
void buildStarted(Gradle gradle) {
println "Build listener buildStarted"
}
@Override
void settingsEvaluated(Settings settings) {
println "Build listener settingsEvaluated"
}
@Override
void projectsLoaded(Gradle gradle) {
println "Build listener projectsLoaded"
}
@Override
void projectsEvaluated(Gradle gradle) {
println "Build listener projectsEvaluated"
}
@Override
void buildFinished(BuildResult result) {
println "Build listener buildFinished"
}
})
task runGradle{
println "configure runGradle AAAAAA"
doFirst {
println "doFirst runGradle AAAAAA"
}
}
代码解析
最上面那段代码就是上一篇文章也写过相同的,随后为Gradle设置了配置监听以及运行监听。然后我们运行一下这个runGradle
任务看下效果:
Starting Gradle Daemon...
Connected to the target VM, address: '127.0.0.1:65159', transport: 'socket'
Gradle Daemon started in 2 s 697 ms
> Configure project :
configure runGradle AAAAAA
Configure listener afterEvaluate
gradle.afterProject
afterEvaluate
> Configure project :app
Configure listener beforeEvaluate
gradle.beforeProject
Configure listener afterEvaluate
gradle.afterProject
Build listener projectsEvaluated
gradle.taskGraph.whenReady
> Task :runGradle
doFirst runGradle AAAAAA
Build listener buildFinished
BUILD SUCCESSFUL in 8s
1 actionable task: 1 executed
14:46:28: Task execution finished 'runGradle'.
Disconnected from the target VM, address: '127.0.0.1:65159', transport: 'socket'
从这个运行效果可以看出,配置阶段它暂时分为了两个(因为现在只有Project以及app的Gradle),在配置project.gradle
的时候,并没有执行beforeEvaluate
和beforeProject
这两个方法;而这两个方法却在配置app.gradle
的时候执行了。
所以上一篇留下的小瑕疵在这里得到了最终解释(为什么配置阶段没运行那两方法),因为在配置project.gradle
的时候,是不会运行那两方法的。
现在继续回到运行效果这里,这次重点放在前三句以及末尾几句。
我们在使用AndroidStudio编译项目的时候,往往都是第一次编译的很慢,但只要编译好了,当天再次编译的时候就非常快;而编译好的项目长时间不编译也会出现编译很慢的情况,这是什么原因呢?
答案就在于:Starting Gradle Daemon...
这段代码。
1.2 Gradle守护进程(Daemon)
项目启动时,会开启一个client,然后启动一个Daemon,通过client向daemon收发请求,项目关闭,client关闭,Daemon保持启动,有类似项目再次部署时,会直接通过新的client访问已经启动的Daemon,所以速度很快,默认daemon不使用3小时后关闭;不同项目兼容性考虑,也可使用--no-daemon 启动项目,就没有速度优势了。
所以在这个运行效果里面能看到: Connected to the target VM, address
运行开始,连接Daemon Disconnected from the target VM, address
运行结束,关闭连接Daemon
在我们使用Gradle的时候,当有多个library工程项目时,往往会对版本进行统一化,因此这就需要使用Gradle属性的扩展功能。
1.3 Gradle属性扩展
-
使用ext对任意对象属性进行扩展:
- 对project进行使用ext进行属性扩展,对所有子project可见。
- 一般在root project中进行ext属性扩展,为子工程提供复用属性,通过rootProject直接访问
- 任意对象都可以使用ext来添加属性:使用闭包,在闭包中定义扩展属性。直接使用=赋值,添加扩展属性。
- 由谁进行ext调用,就属于谁的扩展属性。
- 在build.gradle中,默认是当前工程的project对象,所以在build.gradle直接使用"ext="或者"ext{}"其实就是给project定义扩展属性
使用gradle.properties以键值对形式定义属性,所有project可直接使用
1.3.1 使用ext对任意对象属性进行扩展
在project.gradle里添加如下代码
ext {// project 属性扩展,能在别的工程可见
prop1 = "prop1"
prop3 = "prop3"
}
ext.prop2 = "prop2"
println prop1
println prop2
task runProExtPro{
println "runProExtPro\t"+project.ext.prop3
println "runProExtPro\t"+project.prop2
}
运行任务runProExtPro后的效果
...略
prop1
prop2
runProExtPro prop3
runProExtPro prop2
...略
从这个运行效果可知通过ext
这个属性会开启一个闭包,在闭包内可以进行多属性扩展,扩展后,也可在外部进行单属性扩展。因为这里访问是在当前project.gradle
环境下运行的,现在在app.gradle
里面访问试试。
task runAppExtPro{
println "runAppExtPro\t"+project.prop3
println "runAppExtPro\t"+project.prop2
}
注意看,这里已经把ext
给去掉了,因为在这加上会提示对应属性不存在,所以在访问ext
扩展属性时,推荐直接通过project.xx
的方式直接访问。现在来看看运行runAppExtPro
效果:
...略
runAppExtPro prop3
runAppExtPro prop2
...略
从这里可以看出:对project进行使用ext进行属性扩展,对所有子project可见。
当我们配置版本信息的时候,不想吧扩展属性,配置在根project.gradle里面的时该怎么办呢?此时就有了另一种扩展方式。
1.3.2 使用gradle.properties定义属性
打开gradle.properties
,在里面添加如下属性:
MIN_SDK_VERSION=21
TARGET_SDK_VERSION=30
COMPILE_SDK_VERSION=30
BUILD_TOOL_VERSION=30.0.3
打开对应子project.gradle
或者我们依赖的library
库,就可以使用我们刚刚扩展的属性。
android {
compileSdkVersion Integer.parseInt(COMPILE_SDK_VERSION)
buildToolsVersion BUILD_TOOL_VERSION
defaultConfig {
applicationId "com.hqk.gradledemo01"
minSdkVersion Integer.parseInt(MIN_SDK_VERSION)
targetSdkVersion Integer.parseInt(TARGET_SDK_VERSION)
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
...略
...略
现在依然能够编译成,并且所有子project都可以统一使用 gradle.properties
扩展的属性版本号。当然我们也可以专门写一个task来验证一下: 在app.gradle里,添加如下代码
task checkVersion{
println "runAppGradle"
println "MIN_SDK_VERSION:"+MIN_SDK_VERSION
println "TARGET_SDK_VERSION:"+TARGET_SDK_VERSION
println "COMPILE_SDK_VERSION:"+COMPILE_SDK_VERSION
println "BUILD_TOOL_VERSION:"+BUILD_TOOL_VERSION
}
运行效果
...略
runAppGradle
MIN_SDK_VERSION:21
TARGET_SDK_VERSION:30
COMPILE_SDK_VERSION:30
BUILD_TOOL_VERSION:30.0.3
...略
完美运行,也打出来想要的效果。不过到这为止,写的task几乎都是打印输出,都还没写过复杂逻辑。那么如果想要实现复杂逻辑,要怎样定义task呢?
1.4 Gradle自定义任务
在build.gradle中自定义任务:
- task定义的任务其实就是DefaultTask的一种具体实现类的对象
- 可以使用自定义类继承DeaflutTask:
- 在方法上使用@TaskAction注解,表示任务运行时调用的方法。
- 使用@Input表示对任务的输入参数。
- 使用@OutputFile表示任务输出文件。
- 使用inputs,outputs直接设置任务输入/输出项。
- 一个任务的输出项可以作为另一个任务的输入项 (隐式依赖关系)。
1.4.1 文件数据写入Demo
class WriteTask extends DefaultTask {
@Input
// @Optional
// 表示可选
String from
@OutputFile
// @Optional
// 表示可选
File out
WriteTask() {
}
@TaskAction
void fun() {
println " @TaskAction fun()"
println from
println out.toString()
out.createNewFile()
out.text=from
}
}
task myTask(type: WriteTask) {
from = "a/b/c" // 输入
out = file("test.txt") // 输出
}
从这段代码可知,定义了一个WriteTask
自定义任务,里面两个属性,分别用对应注解表示输入输出对象,随后定义了myTask
任务,将字符串写入file
文件里,运行来看看效果。
如图所示
当Gradle运行成功时,同级目录下新增了txt文件,里面的内容就是我们刚刚写入字符串。现在这个demo来升级一下,目前是一个字符串写入文件,那么能不能将一个文件的内容写入在另一个文件里呢?现在来试试:
class WriteTask extends DefaultTask {
//// @Input
//// @Optional
// // 表示可选
// String from
//// @OutputFile
//// @Optional
// // 表示可选
// File out
WriteTask() {
}
@TaskAction
void fun() {
println " @TaskAction fun()"
// println from
// println out.toString()
// out.createNewFile()
// out.text=from
println inputs.files.singleFile
def inFile = inputs.files.singleFile
def file = outputs.files.singleFile
file.createNewFile()
file.text = inFile.text
}
}
task myTask(type: WriteTask) {
// from = "a/b/c" // 输入
// out = file("test.txt") // 输出
inputs.file file('build.gradle')
outputs.file file('test.txt')
}
现在将输入输出的方式改了,通过inputs.
与outputs.
的方式进行输入输出。里面逻辑是将build.gradle
里面的内容写入test.txt
里面,运行看看效果:
从这里看出,已经成功将build.gradle
里面的内容写入test.txt
里面了。
到这里,数据写入demo 已经写完了。现在开始新的demo:文件压缩
1.4.2 文件压缩Demo
在app.gradle
里面添加如下代码:
task zip(type: Zip) {
archiveName "outputs.zip"// 输出的文件名字
destinationDir file("${buildDir}/custom")// 输出的文件存放的文件夹
from "${buildDir}/outputs"// 输入的文件
}
通过这段代码可知,将会吧同级目录下的${buildDir}
运行成功的build目录下的outputs文件里面的内容进行压缩处理。
注意:这里之所以会压缩,注意型参,类型为Zip
,表示启用的是Zip压缩任务。就和我们刚刚自定义的文件写入形参类型为type: WriteTask
现在运行task zip
看看效果:
从这个效果图可知:这个压缩已经成功压缩了。但问题来了,因为单独执行task zip
任务是不会启用APK编译的,因为两者并没有任何关联(上一篇讲解过),那么如果压缩的目标不存在(apk并没有编译生成对应的build文件夹)会怎样?吧目标文件夹删除试一下:
运行效果
...略
> Task :app:zip NO-SOURCE
Build listener buildFinished
注意看,这里提示 NO-SOURCE
,并没有任何资源,也就是压缩失败了。那么能不能等压缩目标创建 好了再来压缩呢?或者说,执行压缩任务的时候,就算目标任务不存在也要提前编译好后再来压缩。
现在继续改造代码:
//task zip(type: Zip) {
// archiveName "outputs.zip"// 输出的文件名字
// destinationDir file("${buildDir}/custom")// 输出的文件存放的文件夹
// from "${buildDir}/outputs"// 输入的文件
//}
afterEvaluate {
println tasks.getByName("packageDebug")
task zip(type: Zip) {
archiveName "outputs2.zip"// 输出的文件名字
destinationDir file("${buildDir}/custom")// 输出的文件存放的文件夹
from tasks.getByName("packageDebug").outputs.files// 输入的文件
tasks.getByName("packageDebug").outputs.files.each {
println it
}
}
}
在这里我将压缩任务转移到了app.gradle
的afterEvaluate
闭包里面,也就是说,apk在编译配置即将结束的时候,会将task zip
任务,注入在Gradle执行流程里,当单独运行task zip
任务的时候,因为它在apk编译执行流程里面,所以它就会启动apk的编译,随后执行task zip
任务就能达到想要的效果了。现在继续单独运行task zip
任务试试:
注意:形参type: Zip的任务只能存在一个,所以要把外面的注释掉
代码这没有运行按钮了,那么就用右边工具来辅助运行,注意左边并没有编译好的文件夹,点击右边运行:
运行结束后,左边如愿以偿多了对应的build文件夹,里面也有对应的压缩包,而且名字也能对上。
到这里文件压缩demo已经完美的实现了,但是这个功能只能给你自己这一个项目使用,那万一想给他人使用或者说给其他项目使用怎么办呢?那这个就遇到用到插件了。
2.Gradle插件
2.1 什么是Gradle插件
- Gradle插件是提供给gradle构建工具,在编译时使用的依赖项。插件的本质就是对公用的构建业务进行打包,以提供复用
- Gradle插件分为:脚本插件和二进制插件 (实现Plugin的类)
- Gradle插件通过apply方法引入到工程
这里说到Gradle插件分为:脚本插件和二进制插件,那么对应有何区别?
- 脚本插件实现了一些列的任务,并且进行了组装,按照提供的API就可以直接使用
- Gradle脚本插件,是提供实现的任务封装,需要自行组装。或者是用到的一些具体业务的封装。
2.2 Gradle 脚本插件
既然是脚本,那么就创建对应的脚本:在项目根目录创建脚本文件script.gradle
,里面写入代码:
afterEvaluate {
println tasks.getByName("packageDebug")
task zip(type: Zip) {
archiveName "outputs3.zip"// 输出的文件名字
destinationDir file("${buildDir}/custom")// 输出的文件存放的文件夹
from tasks.getByName("packageDebug").outputs.files// 输入的文件
tasks.getByName("packageDebug").outputs.files.each {
println it
}
}
}
仔细 看这个脚本,可以发现:脚本插件里面的内容和刚刚我们在app.gradle
里面写入的内容一模一样,接下来按照apply方法引入到工程试试:
进入app.gradle里面
apply from: '../script.gradle'
android {
compileSdkVersion Integer.parseInt(COMPILE_SDK_VERSION)
buildToolsVersion BUILD_TOOL_VERSION
...略
}
//task zip(type: Zip) {
// archiveName "outputs.zip"// 输出的文件名字
// destinationDir file("${buildDir}/custom")// 输出的文件存放的文件夹
// from "${buildDir}/outputs"// 输入的文件
//}
//afterEvaluate {
// println tasks.getByName("packageDebug")
// task zip(type: Zip) {
// archiveName "outputs2.zip"// 输出的文件名字
// destinationDir file("${buildDir}/custom")// 输出的文件存放的文件夹
// from tasks.getByName("packageDebug").outputs.files// 输入的文件
// tasks.getByName("packageDebug").outputs.files.each {
// println it
// }
// }
//}
记得这里要把刚刚的压缩注释掉。现在继续点击右边的运行看看效果:
从这个效果上看,已经完美运行成功!脚本插件就这么简单!那么二进制插件又该是怎样的?
2.3 Gradle 二进制插件
//apply from: '../script.gradle'
apply plugin: MyPlugin
android {
compileSdkVersion Integer.parseInt(COMPILE_SDK_VERSION)
buildToolsVersion BUILD_TOOL_VERSION
...略
}
//task zip(type: Zip) {
// archiveName "outputs.zip"// 输出的文件名字
// destinationDir file("${buildDir}/custom")// 输出的文件存放的文件夹
// from "${buildDir}/outputs"// 输入的文件
//}
//afterEvaluate {
// println tasks.getByName("packageDebug")
// task zip(type: Zip) {
// archiveName "outputs2.zip"// 输出的文件名字
// destinationDir file("${buildDir}/custom")// 输出的文件存放的文件夹
// from tasks.getByName("packageDebug").outputs.files// 输入的文件
// tasks.getByName("packageDebug").outputs.files.each {
// println it
// }
// }
//}
//=============================================
// 插件:1. 脚本插件
// 2. 二进制插件
class MyPlugin implements Plugin {
@Override
void apply(Project target) {
println "MyPlugin apply"
target.afterEvaluate {
println "MyPlugin afterEvaluate "+target.tasks.getByName("packageDebug")
target.task(type: Zip, "zip") {//第二个参数要指定是哪个方法
archiveName "outputs4.zip"// 输出的文件名字
destinationDir target.file("${target.buildDir}/custom")// 输出的文件存放的文件夹
from target.tasks.getByName("packageDebug").outputs.files// 输入的文件
target.tasks.getByName("packageDebug").outputs.files.each {
println it
}
}
}
}
这里看到,定义了MyPlugin
类实现了对应的Plugin
接口,在对应的target.afterEvaluate
里面定义了任务target.task(type: Zip, "zip")
,第一个参数明确什么类型,第二个参数表示当前任务名为zip
压缩。
现在删除之前运行的结果,继续运行右边的任务,看看效果:
哈哈哈,这个插件也如期的运行成功了。到这里这篇教程差不多就结束了。
3. 结束语
相信看到这里的小伙伴,对Gradle的核心模型以及Gradle插件有了一个全新的认知。在下一篇里,将会继续深入Gradle讲解。
原创不易,如果本篇文章对小伙伴们有用,希望小伙伴们多多点赞支持一下。笔者也好更快更好的更新教程。
本文转自 https://juejin.cn/post/7024069982325571592,如有侵权,请联系删除。