前言
在上一篇文章中,對(duì)Gradle基礎(chǔ)以及構(gòu)建機(jī)制進(jìn)行了詳細(xì)的講解,在這一篇中將會(huì)對(duì)Gradle核心模型以及Gradle插件進(jìn)行講解。
1.Gradle核心模型
1.1 Gradle鉤子函數(shù)
講鉤子函數(shù),還是得拿出Gradle執(zhí)行流程圖

如圖所示
- gradle在生命周期三個(gè)階段都設(shè)置了相應(yīng)的鉤子函數(shù)調(diào)用。
- 使用鉤子函數(shù),處理自定義的構(gòu)建:
- 初始化階段:gradle.settingsEvaluated和gradle.projectsLoaded。(在settings.gradle中生效)
- 配置階段:project.beforeEvaluate和project.afterEvaluate;gradle.beforeProject、gradle.afterProject及gradle.taskGraph.taskGraph.whenReady。
- 執(zhí)行階段:gradle.taskGraph.beforeTask和gradle.taskGraph.afterTask。
而Gradle也可以監(jiān)聽各個(gè)階段的回調(diào)處理:
- gradle.addProjectEvaluationListener
- gradle.addBuildListener
- gradle.addListener:TaskExecutionGraphListener (任務(wù)執(zhí)行圖監(jiān)聽),TaskExecutionListener(任務(wù)執(zhí)行監(jiān)聽),TaskExecutionListener、TaskActionListener、StandardOutputListener ...
概念又說了一大堆,擼碼驗(yàn)證一下!
- 打開AS,創(chuàng)建一個(gè)普通工程項(xiàng)目。
- 進(jìn)入項(xiàng)目
build.gradle(外層)文件 - 在末尾添加如下代碼:
// =======================================
// Gradle提供的鉤子函數(shù)
// 配置階段:
gradle.beforeProject {
println "gradle.beforeProject"
}
gradle.afterProject {
println "gradle.afterProject"
}
gradle.taskGraph.whenReady {
println "gradle.taskGraph.whenReady"
}
beforeEvaluate {
println "beforeEvaluate"
}
afterEvaluate {
println "afterEvaluate"
}
//==================
// 為gradle設(shè)置監(jiān)聽
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設(shè)置了配置監(jiān)聽以及運(yùn)行監(jiān)聽。然后我們運(yùn)行一下這個(gè)runGradle任務(wù)看下效果:
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'
從這個(gè)運(yùn)行效果可以看出,配置階段它暫時(shí)分為了兩個(gè)(因?yàn)楝F(xiàn)在只有Project以及app的Gradle),在配置project.gradle的時(shí)候,并沒有執(zhí)行beforeEvaluate和beforeProject這兩個(gè)方法;而這兩個(gè)方法卻在配置app.gradle的時(shí)候執(zhí)行了。
所以上一篇留下的小瑕疵在這里得到了最終解釋(為什么配置階段沒運(yùn)行那兩方法),因?yàn)樵谂渲?code>project.gradle的時(shí)候,是不會(huì)運(yùn)行那兩方法的。
現(xiàn)在繼續(xù)回到運(yùn)行效果這里,這次重點(diǎn)放在前三句以及末尾幾句。
我們?cè)谑褂肁ndroidStudio編譯項(xiàng)目的時(shí)候,往往都是第一次編譯的很慢,但只要編譯好了,當(dāng)天再次編譯的時(shí)候就非常快;而編譯好的項(xiàng)目長時(shí)間不編譯也會(huì)出現(xiàn)編譯很慢的情況,這是什么原因呢?
答案就在于:Starting Gradle Daemon... 這段代碼。
1.2 Gradle守護(hù)進(jìn)程(Daemon)
項(xiàng)目啟動(dòng)時(shí),會(huì)開啟一個(gè)client,然后啟動(dòng)一個(gè)Daemon,通過client向daemon收發(fā)請(qǐng)求,項(xiàng)目關(guān)閉,client關(guān)閉,Daemon保持啟動(dòng),有類似項(xiàng)目再次部署時(shí),會(huì)直接通過新的client訪問已經(jīng)啟動(dòng)的Daemon,所以速度很快,默認(rèn)daemon不使用3小時(shí)后關(guān)閉;不同項(xiàng)目兼容性考慮,也可使用--no-daemon 啟動(dòng)項(xiàng)目,就沒有速度優(yōu)勢(shì)了。
所以在這個(gè)運(yùn)行效果里面能看到: Connected to the target VM, address 運(yùn)行開始,連接Daemon Disconnected from the target VM, address 運(yùn)行結(jié)束,關(guān)閉連接Daemon
在我們使用Gradle的時(shí)候,當(dāng)有多個(gè)library工程項(xiàng)目時(shí),往往會(huì)對(duì)版本進(jìn)行統(tǒng)一化,因此這就需要使用Gradle屬性的擴(kuò)展功能。
1.3 Gradle屬性擴(kuò)展
-
使用ext對(duì)任意對(duì)象屬性進(jìn)行擴(kuò)展:
- 對(duì)project進(jìn)行使用ext進(jìn)行屬性擴(kuò)展,對(duì)所有子project可見。
- 一般在root project中進(jìn)行ext屬性擴(kuò)展,為子工程提供復(fù)用屬性,通過rootProject直接訪問
- 任意對(duì)象都可以使用ext來添加屬性:使用閉包,在閉包中定義擴(kuò)展屬性。直接使用=賦值,添加擴(kuò)展屬性。
- 由誰進(jìn)行ext調(diào)用,就屬于誰的擴(kuò)展屬性。
- 在build.gradle中,默認(rèn)是當(dāng)前工程的project對(duì)象,所以在build.gradle直接使用"ext="或者"ext{}"其實(shí)就是給project定義擴(kuò)展屬性
使用gradle.properties以鍵值對(duì)形式定義屬性,所有project可直接使用
1.3.1 使用ext對(duì)任意對(duì)象屬性進(jìn)行擴(kuò)展
在project.gradle里添加如下代碼
ext {// project 屬性擴(kuò)展,能在別的工程可見
prop1 = "prop1"
prop3 = "prop3"
}
ext.prop2 = "prop2"
println prop1
println prop2
task runProExtPro{
println "runProExtPro\t"+project.ext.prop3
println "runProExtPro\t"+project.prop2
}
運(yùn)行任務(wù)runProExtPro后的效果
...略
prop1
prop2
runProExtPro prop3
runProExtPro prop2
...略
從這個(gè)運(yùn)行效果可知通過ext這個(gè)屬性會(huì)開啟一個(gè)閉包,在閉包內(nèi)可以進(jìn)行多屬性擴(kuò)展,擴(kuò)展后,也可在外部進(jìn)行單屬性擴(kuò)展。因?yàn)檫@里訪問是在當(dāng)前project.gradle環(huán)境下運(yùn)行的,現(xiàn)在在app.gradle里面訪問試試。
task runAppExtPro{
println "runAppExtPro\t"+project.prop3
println "runAppExtPro\t"+project.prop2
}
注意看,這里已經(jīng)把ext給去掉了,因?yàn)樵谶@加上會(huì)提示對(duì)應(yīng)屬性不存在,所以在訪問ext擴(kuò)展屬性時(shí),推薦直接通過project.xx的方式直接訪問。現(xiàn)在來看看運(yùn)行runAppExtPro效果:
...略
runAppExtPro prop3
runAppExtPro prop2
...略
從這里可以看出:對(duì)project進(jìn)行使用ext進(jìn)行屬性擴(kuò)展,對(duì)所有子project可見。
當(dāng)我們配置版本信息的時(shí)候,不想吧擴(kuò)展屬性,配置在根project.gradle里面的時(shí)該怎么辦呢?此時(shí)就有了另一種擴(kuò)展方式。
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
打開對(duì)應(yīng)子project.gradle或者我們依賴的library庫,就可以使用我們剛剛擴(kuò)展的屬性。
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"
}
...略
...略
現(xiàn)在依然能夠編譯成,并且所有子project都可以統(tǒng)一使用 gradle.properties擴(kuò)展的屬性版本號(hào)。當(dāng)然我們也可以專門寫一個(gè)task來驗(yàn)證一下: 在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
}
運(yùn)行效果
...略
runAppGradle
MIN_SDK_VERSION:21
TARGET_SDK_VERSION:30
COMPILE_SDK_VERSION:30
BUILD_TOOL_VERSION:30.0.3
...略
完美運(yùn)行,也打出來想要的效果。不過到這為止,寫的task幾乎都是打印輸出,都還沒寫過復(fù)雜邏輯。那么如果想要實(shí)現(xiàn)復(fù)雜邏輯,要怎樣定義task呢?
1.4 Gradle自定義任務(wù)
在build.gradle中自定義任務(wù):
- task定義的任務(wù)其實(shí)就是DefaultTask的一種具體實(shí)現(xiàn)類的對(duì)象
- 可以使用自定義類繼承DeaflutTask:
- 在方法上使用@TaskAction注解,表示任務(wù)運(yùn)行時(shí)調(diào)用的方法。
- 使用@Input表示對(duì)任務(wù)的輸入?yún)?shù)。
- 使用@OutputFile表示任務(wù)輸出文件。
- 使用inputs,outputs直接設(shè)置任務(wù)輸入/輸出項(xiàng)。
- 一個(gè)任務(wù)的輸出項(xiàng)可以作為另一個(gè)任務(wù)的輸入項(xiàng) (隱式依賴關(guān)系)。
1.4.1 文件數(shù)據(jù)寫入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") // 輸出
}
從這段代碼可知,定義了一個(gè)WriteTask自定義任務(wù),里面兩個(gè)屬性,分別用對(duì)應(yīng)注解表示輸入輸出對(duì)象,隨后定義了myTask 任務(wù),將字符串寫入file文件里,運(yùn)行來看看效果。

如圖所示
當(dāng)Gradle運(yùn)行成功時(shí),同級(jí)目錄下新增了txt文件,里面的內(nèi)容就是我們剛剛寫入字符串?,F(xiàn)在這個(gè)demo來升級(jí)一下,目前是一個(gè)字符串寫入文件,那么能不能將一個(gè)文件的內(nèi)容寫入在另一個(gè)文件里呢?現(xiàn)在來試試:
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')
}
現(xiàn)在將輸入輸出的方式改了,通過inputs.與outputs.的方式進(jìn)行輸入輸出。里面邏輯是將build.gradle里面的內(nèi)容寫入test.txt里面,運(yùn)行看看效果:

從這里看出,已經(jīng)成功將build.gradle里面的內(nèi)容寫入test.txt里面了。
到這里,數(shù)據(jù)寫入demo 已經(jīng)寫完了?,F(xiàn)在開始新的demo:文件壓縮
1.4.2 文件壓縮Demo
在app.gradle里面添加如下代碼:
task zip(type: Zip) {
archiveName "outputs.zip"http:// 輸出的文件名字
destinationDir file("${buildDir}/custom")// 輸出的文件存放的文件夾
from "${buildDir}/outputs"http:// 輸入的文件
}
通過這段代碼可知,將會(huì)吧同級(jí)目錄下的${buildDir}運(yùn)行成功的build目錄下的outputs文件里面的內(nèi)容進(jìn)行壓縮處理。
注意:這里之所以會(huì)壓縮,注意型參,類型為Zip,表示啟用的是Zip壓縮任務(wù)。就和我們剛剛自定義的文件寫入形參類型為type: WriteTask
現(xiàn)在運(yùn)行task zip看看效果:

從這個(gè)效果圖可知:這個(gè)壓縮已經(jīng)成功壓縮了。但問題來了,因?yàn)閱为?dú)執(zhí)行task zip任務(wù)是不會(huì)啟用APK編譯的,因?yàn)閮烧卟]有任何關(guān)聯(lián)(上一篇講解過),那么如果壓縮的目標(biāo)不存在(apk并沒有編譯生成對(duì)應(yīng)的build文件夾)會(huì)怎樣?吧目標(biāo)文件夾刪除試一下:
運(yùn)行效果
...略
> Task :app:zip NO-SOURCE
Build listener buildFinished
注意看,這里提示 NO-SOURCE,并沒有任何資源,也就是壓縮失敗了。那么能不能等壓縮目標(biāo)創(chuàng)建 好了再來壓縮呢?或者說,執(zhí)行壓縮任務(wù)的時(shí)候,就算目標(biāo)任務(wù)不存在也要提前編譯好后再來壓縮。
現(xiàn)在繼續(xù)改造代碼:
//task zip(type: Zip) {
// archiveName "outputs.zip"http:// 輸出的文件名字
// destinationDir file("${buildDir}/custom")// 輸出的文件存放的文件夾
// from "${buildDir}/outputs"http:// 輸入的文件
//}
afterEvaluate {
println tasks.getByName("packageDebug")
task zip(type: Zip) {
archiveName "outputs2.zip"http:// 輸出的文件名字
destinationDir file("${buildDir}/custom")// 輸出的文件存放的文件夾
from tasks.getByName("packageDebug").outputs.files// 輸入的文件
tasks.getByName("packageDebug").outputs.files.each {
println it
}
}
}
在這里我將壓縮任務(wù)轉(zhuǎn)移到了app.gradle的afterEvaluate 閉包里面,也就是說,apk在編譯配置即將結(jié)束的時(shí)候,會(huì)將task zip任務(wù),注入在Gradle執(zhí)行流程里,當(dāng)單獨(dú)運(yùn)行task zip任務(wù)的時(shí)候,因?yàn)樗赼pk編譯執(zhí)行流程里面,所以它就會(huì)啟動(dòng)apk的編譯,隨后執(zhí)行task zip任務(wù)就能達(dá)到想要的效果了。現(xiàn)在繼續(xù)單獨(dú)運(yùn)行task zip任務(wù)試試:
注意:形參type: Zip的任務(wù)只能存在一個(gè),所以要把外面的注釋掉

代碼這沒有運(yùn)行按鈕了,那么就用右邊工具來輔助運(yùn)行,注意左邊并沒有編譯好的文件夾,點(diǎn)擊右邊運(yùn)行:

運(yùn)行結(jié)束后,左邊如愿以償多了對(duì)應(yīng)的build文件夾,里面也有對(duì)應(yīng)的壓縮包,而且名字也能對(duì)上。
到這里文件壓縮demo已經(jīng)完美的實(shí)現(xiàn)了,但是這個(gè)功能只能給你自己這一個(gè)項(xiàng)目使用,那萬一想給他人使用或者說給其他項(xiàng)目使用怎么辦呢?那這個(gè)就遇到用到插件了。
2.Gradle插件
2.1 什么是Gradle插件
- Gradle插件是提供給gradle構(gòu)建工具,在編譯時(shí)使用的依賴項(xiàng)。插件的本質(zhì)就是對(duì)公用的構(gòu)建業(yè)務(wù)進(jìn)行打包,以提供復(fù)用
- Gradle插件分為:腳本插件和二進(jìn)制插件 (實(shí)現(xiàn)Plugin的類)
- Gradle插件通過apply方法引入到工程
這里說到Gradle插件分為:腳本插件和二進(jìn)制插件,那么對(duì)應(yīng)有何區(qū)別?
- 腳本插件實(shí)現(xiàn)了一些列的任務(wù),并且進(jìn)行了組裝,按照提供的API就可以直接使用
- Gradle腳本插件,是提供實(shí)現(xiàn)的任務(wù)封裝,需要自行組裝?;蛘呤怯玫降囊恍┚唧w業(yè)務(wù)的封裝。
2.2 Gradle 腳本插件
既然是腳本,那么就創(chuàng)建對(duì)應(yīng)的腳本:在項(xiàng)目根目錄創(chuàng)建腳本文件script.gradle,里面寫入代碼:
afterEvaluate {
println tasks.getByName("packageDebug")
task zip(type: Zip) {
archiveName "outputs3.zip"http:// 輸出的文件名字
destinationDir file("${buildDir}/custom")// 輸出的文件存放的文件夾
from tasks.getByName("packageDebug").outputs.files// 輸入的文件
tasks.getByName("packageDebug").outputs.files.each {
println it
}
}
}
仔細(xì) 看這個(gè)腳本,可以發(fā)現(xiàn):腳本插件里面的內(nèi)容和剛剛我們?cè)?code>app.gradle里面寫入的內(nèi)容一模一樣,接下來按照apply方法引入到工程試試:
進(jìn)入app.gradle里面
apply from: '../script.gradle'
android {
compileSdkVersion Integer.parseInt(COMPILE_SDK_VERSION)
buildToolsVersion BUILD_TOOL_VERSION
...略
}
//task zip(type: Zip) {
// archiveName "outputs.zip"http:// 輸出的文件名字
// destinationDir file("${buildDir}/custom")// 輸出的文件存放的文件夾
// from "${buildDir}/outputs"http:// 輸入的文件
//}
//afterEvaluate {
// println tasks.getByName("packageDebug")
// task zip(type: Zip) {
// archiveName "outputs2.zip"http:// 輸出的文件名字
// destinationDir file("${buildDir}/custom")// 輸出的文件存放的文件夾
// from tasks.getByName("packageDebug").outputs.files// 輸入的文件
// tasks.getByName("packageDebug").outputs.files.each {
// println it
// }
// }
//}
記得這里要把剛剛的壓縮注釋掉?,F(xiàn)在繼續(xù)點(diǎn)擊右邊的運(yùn)行看看效果:

從這個(gè)效果上看,已經(jīng)完美運(yùn)行成功!腳本插件就這么簡單!那么二進(jìn)制插件又該是怎樣的?
2.3 Gradle 二進(jìn)制插件
//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"http:// 輸出的文件名字
// destinationDir file("${buildDir}/custom")// 輸出的文件存放的文件夾
// from "${buildDir}/outputs"http:// 輸入的文件
//}
//afterEvaluate {
// println tasks.getByName("packageDebug")
// task zip(type: Zip) {
// archiveName "outputs2.zip"http:// 輸出的文件名字
// destinationDir file("${buildDir}/custom")// 輸出的文件存放的文件夾
// from tasks.getByName("packageDebug").outputs.files// 輸入的文件
// tasks.getByName("packageDebug").outputs.files.each {
// println it
// }
// }
//}
//=============================================
// 插件:1. 腳本插件
// 2. 二進(jìn)制插件
class MyPlugin implements Plugin<Project> {
@Override
void apply(Project target) {
println "MyPlugin apply"
target.afterEvaluate {
println "MyPlugin afterEvaluate "+target.tasks.getByName("packageDebug")
target.task(type: Zip, "zip") {//第二個(gè)參數(shù)要指定是哪個(gè)方法
archiveName "outputs4.zip"http:// 輸出的文件名字
destinationDir target.file("${target.buildDir}/custom")// 輸出的文件存放的文件夾
from target.tasks.getByName("packageDebug").outputs.files// 輸入的文件
target.tasks.getByName("packageDebug").outputs.files.each {
println it
}
}
}
}
這里看到,定義了MyPlugin 類實(shí)現(xiàn)了對(duì)應(yīng)的Plugin<Project> 接口,在對(duì)應(yīng)的target.afterEvaluate里面定義了任務(wù)target.task(type: Zip, "zip"),第一個(gè)參數(shù)明確什么類型,第二個(gè)參數(shù)表示當(dāng)前任務(wù)名為zip壓縮。
現(xiàn)在刪除之前運(yùn)行的結(jié)果,繼續(xù)運(yùn)行右邊的任務(wù),看看效果:

哈哈哈,這個(gè)插件也如期的運(yùn)行成功了。到這里這篇教程差不多就結(jié)束了。
3. 結(jié)束語
相信看到這里的小伙伴,對(duì)Gradle的核心模型以及Gradle插件有了一個(gè)全新的認(rèn)知。在下一篇里,將會(huì)繼續(xù)深入Gradle講解。
原創(chuàng)不易,如果本篇文章對(duì)小伙伴們有用,希望小伙伴們多多點(diǎn)贊支持一下。筆者也好更快更好的更新教程。
本文轉(zhuǎn)自 https://juejin.cn/post/7024069982325571592,如有侵權(quán),請(qǐng)聯(lián)系刪除。