Gradle基礎(chǔ) - 構(gòu)建生命周期和Hook技術(shù)

對于初學(xué)者來說,面對各種各樣的Gradle構(gòu)建腳本,想要梳理它的構(gòu)建流程,往往不知道從何入手。Gradle的構(gòu)建過程有著固定的生命周期,理解Gradle的生命周期和Hook點,有助于幫你梳理、擴(kuò)展項目的構(gòu)建流程。

構(gòu)建的生命周期

任何Gradle的構(gòu)建過程都分為三部分:初始化階段、配置階段和執(zhí)行階段。

初始化階段

初始化階段的任務(wù)是創(chuàng)建項目的層次結(jié)構(gòu),并且為每一個項目創(chuàng)建一個Project實例。
與初始化階段相關(guān)的腳本文件是settings.gradle(包括<USER_HOME>/.gradle/init.d目錄下的所有.gradle腳本文件,這些文件作用于本機(jī)的所有構(gòu)建過程)。一個settings.gradle腳本對應(yīng)一個Settings對象,我們最常用來聲明項目的層次結(jié)構(gòu)的include就是Settings類下的一個方法,在Gradle初始化的時候會構(gòu)造一個Settings實例對象,它包含了下圖中的方法,這些方法都可以直接在settings.gradle中直接訪問。

Settings.java

比如可以通過如下代碼向Gradle的構(gòu)建過程添加監(jiān)聽:

gradle.addBuildListener(new BuildListener() {
  void buildStarted(Gradle var1) {
    println '開始構(gòu)建'
  }
  void settingsEvaluated(Settings var1) {
    println 'settings評估完成(settins.gradle中代碼執(zhí)行完畢)'
    // var1.gradle.rootProject 這里訪問Project對象時會報錯,還未完成Project的初始化
  }
  void projectsLoaded(Gradle var1) {
    println '項目結(jié)構(gòu)加載完成(初始化階段結(jié)束)'
    println '初始化結(jié)束,可訪問根項目:' + var1.gradle.rootProject
  }
  void projectsEvaluated(Gradle var1) {
    println '所有項目評估完成(配置階段結(jié)束)'
  }
  void buildFinished(BuildResult var1) {
    println '構(gòu)建結(jié)束 '
  }
})

執(zhí)行gradle build,打印結(jié)果如下:

settings評估完成(settins.gradle中代碼執(zhí)行完畢)
項目結(jié)構(gòu)加載完成(初始化階段結(jié)束)
初始化結(jié)束,可訪問根項目:root project 'GradleTest'
所有項目評估完成(配置階段結(jié)束)
:buildEnvironment

------------------------------------------------------------
Root project
------------------------------------------------------------

classpath
No dependencies

BUILD SUCCESSFUL

Total time: 0.959 secs
構(gòu)建結(jié)束 

配置階段

配置階段的任務(wù)是執(zhí)行各項目下的build.gradle腳本,完成Project的配置,并且構(gòu)造Task任務(wù)依賴關(guān)系圖以便在執(zhí)行階段按照依賴關(guān)系執(zhí)行Task。
該階段也是我們最常接觸到的構(gòu)建階段,比如應(yīng)用外部構(gòu)建插件apply plugin: 'com.android.application',配置插件的屬性android{ compileSdkVersion 25 ...}等。每個build.gralde腳本文件對應(yīng)一個Project對象,在初始化階段創(chuàng)建,Project接口文檔。
配置階段執(zhí)行的代碼包括build.gralde中的各種語句、閉包以及Task中的配置段語句,在根目錄的build.gradle中添加如下代碼:

println 'build.gradle的配置階段'

// 調(diào)用Project的dependencies(Closure c)聲明項目依賴
dependencies {
    // 閉包中執(zhí)行的代碼
    println 'dependencies中執(zhí)行的代碼'
}

// 創(chuàng)建一個Task
task test() {
  println 'Task中的配置代碼'
  // 定義一個閉包
  def a = {
    println 'Task中的配置代碼2'
  }
  // 執(zhí)行閉包
  a()
  doFirst {
    println '這段代碼配置階段不執(zhí)行'
  }
}

println '我是順序執(zhí)行的'

調(diào)用gradle build,得到如下結(jié)果:

build.gradle的配置階段
dependencies中執(zhí)行的代碼
Task中的配置代碼
Task中的配置代碼2
我是順序執(zhí)行的
:buildEnvironment

------------------------------------------------------------
Root project
------------------------------------------------------------

classpath
No dependencies

BUILD SUCCESSFUL

Total time: 1.144 secs

一定要注意,配置階段不僅執(zhí)行build.gradle中的語句,還包括了Task中的配置語句。從上面執(zhí)行結(jié)果中可以看到,在執(zhí)行了dependencies的閉包后,直接執(zhí)行的是任務(wù)test中的配置段代碼(Task中除了Action外的代碼段都在配置階段執(zhí)行)。
另外一點,無論執(zhí)行Gradle的任何命令,初始化階段和配置階段的代碼都會被執(zhí)行。
同樣是上面那段Gradle腳本,我們執(zhí)行幫助任務(wù)gradle help,任然會打印出上面的執(zhí)行結(jié)果。我們在排查構(gòu)建速度問題的時候可以留意,是否部分代碼可以寫成任務(wù)Task,從而減少配置階段消耗的時間。

執(zhí)行階段

在配置階段結(jié)束后,Gradle會根據(jù)任務(wù)Task的依賴關(guān)系創(chuàng)建一個有向無環(huán)圖,可以通過Gradle對象的getTaskGraph方法訪問,對應(yīng)的類為TaskExecutionGraph,然后通過調(diào)用gradle <任務(wù)名>執(zhí)行對應(yīng)任務(wù)。

下面我們展示如何調(diào)用子項目中的任務(wù)。

  1. 在根目錄下創(chuàng)建目錄subproject,并添加文件build.gradle
  2. 在settings.gradle中添加include ':subproject'
  3. 在subproject的build.gradle中添加如下代碼
task grandpa {
  doFirst {
    println 'task grandpa:doFirst 先于 doLast 執(zhí)行'
  }
  doLast {
    println 'task grandpa:doLast'
  }
}

task father(dependsOn: grandpa) {
  doLast {
    println 'task father:doLast'
  }
}

task mother << {
  println 'task mother 先于 task father 執(zhí)行'
}

task child(dependsOn: [father, mother]){
  doLast {
    println 'task child 最后執(zhí)行'
  }
}

task nobody {
  doLast {
    println '我不執(zhí)行'
  }
}
// 指定任務(wù)father必須在任務(wù)mother之后執(zhí)行
father.mustRunAfter mother

它們的依賴關(guān)系如下:

:subproject:child
+--- :subproject:father
|    \--- :subproject:grandpa
\--- :subproject:mother

執(zhí)行gradle :subproject:child,得到如下打印結(jié)果:

:subproject:mother
task mother 先于 task father 執(zhí)行
:subproject:grandpa
task grandpa:doFirst 先于 doLast 執(zhí)行
task grandpa:doLast
:subproject:father
task father:doLast
:subproject:child
task child 最后執(zhí)行

BUILD SUCCESSFUL

Total time: 1.005 secs

因為在配置階段,我們聲明了任務(wù)mother的優(yōu)先級高于任務(wù)father,所以mother先于father執(zhí)行,而任務(wù)father依賴于任務(wù)grandpa,所以grandpa先于father執(zhí)行。任務(wù)nobody不存在于child的依賴關(guān)系中,所以不執(zhí)行。

Hook點

Gradle提供了非常多的鉤子供開發(fā)人員修改構(gòu)建過程中的行為,為了方便說明,先看下面這張圖。

Gradle構(gòu)建周期中的Hook點

Gradle在構(gòu)建的各個階段都提供了很多回調(diào),我們在添加對應(yīng)監(jiān)聽時要注意,監(jiān)聽器一定要在回調(diào)的生命周期之前添加,比如我們在根項目的build.gradle中添加下面的代碼就是錯誤的:

gradle.settingsEvaluated { setting ->
  // do something with setting
}

gradle.projectsLoaded { 
  gradle.rootProject.afterEvaluate {
    println 'rootProject evaluated'
  }
}

當(dāng)構(gòu)建走到build.gradle時說明初始化過程已經(jīng)結(jié)束了,所以上面的回調(diào)都不會執(zhí)行,把上述代碼移動到settings.gradle中就正確了。

下面通過一些例子來解釋如何Hook Gradle的構(gòu)建過程。

  • 為所有子項目添加公共代碼

在根項目的build.gradle中添加如下代碼:

gradle.beforeProject { project ->
  println 'apply plugin java for ' + project
  project.apply plugin: 'java'
}

這段代碼的作用是為所有子項目應(yīng)用Java插件,因為代碼是在根項目的配置階段執(zhí)行的,所以并不會應(yīng)用到根項目中。
這里說明一下Gradle的beforeProject方法和Project的beforeEvaluate的執(zhí)行時機(jī)是一樣的,只是beforeProject應(yīng)用于所有項目,而beforeEvaluate只應(yīng)用于調(diào)用的Project,上面的代碼等價于:

allprojects {
  beforeEvaluate { project ->
    println 'apply plugin java for ' + project
    project.apply plugin: 'java'
  }
}

after***也是同理的,但afterProject還有一點不一樣,無論Project的配置過程是否出錯,afterProject都會收到回調(diào)。

  • 為指定Task動態(tài)添加Action

gradle.taskGraph.beforeTask { task ->
  task << {
    println '動態(tài)添加的Action'
  }
}

task Test {
  doLast {
    println '原始Action'
  }
}

在任務(wù)Test執(zhí)行前,動態(tài)添加了一個doLast動作。

  • 獲取構(gòu)建各階段耗時情況

long beginOfSetting = System.currentTimeMillis()

gradle.projectsLoaded {
  println '初始化階段,耗時:' + (System.currentTimeMillis() - beginOfSetting) + 'ms'
}

def beginOfConfig
def configHasBegin = false
def beginOfProjectConfig = new HashMap()
gradle.beforeProject { project ->
  if (!configHasBegin) {
    configHasBegin = true
    beginOfConfig = System.currentTimeMillis()
  }
  beginOfProjectConfig.put(project, System.currentTimeMillis())
}
gradle.afterProject { project ->
  def begin = beginOfProjectConfig.get(project)
  println '配置階段,' + project + '耗時:' + (System.currentTimeMillis() - begin) + 'ms'
}
def beginOfProjectExcute
gradle.taskGraph.whenReady {
  println '配置階段,總共耗時:' + (System.currentTimeMillis() - beginOfConfig) + 'ms'
  beginOfProjectExcute = System.currentTimeMillis()
}
gradle.taskGraph.beforeTask { task ->
  task.doFirst {
    task.ext.beginOfTask = System.currentTimeMillis()
  }
  task.doLast {
    println '執(zhí)行階段,' + task + '耗時:' + (System.currentTimeMillis() - task.beginOfTask) + 'ms'
  }
}
gradle.buildFinished {
  println '執(zhí)行階段,耗時:' + (System.currentTimeMillis() - beginOfProjectExcute) + 'ms'
}

將上述代碼段添加到settings.gradle腳本文件的開頭,再執(zhí)行任意構(gòu)建任務(wù),你就可以看到各階段、各任務(wù)的耗時情況。

  • 動態(tài)改變Task依賴關(guān)系

有時我們需要在一個已有的構(gòu)建系統(tǒng)中插入我們自己的構(gòu)建任務(wù),比如在執(zhí)行Java構(gòu)建后我們想要刪除構(gòu)建過程中產(chǎn)生的臨時文件,那么我們就可以自定義一個名叫cleanTemp的任務(wù),讓其依賴于build任務(wù),然后調(diào)用cleanTemp任務(wù)即可。
但是這種方式適用范圍太小,比如在使用IDE執(zhí)行構(gòu)建時,IDE默認(rèn)就是調(diào)用build任務(wù),我們沒法修改IDE的行為,所以我們需要將自定義的任務(wù)插入到原有的任務(wù)關(guān)系中。

  1. 尋找插入點
    如果你對一個構(gòu)建的任務(wù)依賴關(guān)系不熟悉的話,可以使用一個插件來查看,在根項目的build.gradle中添加如下代碼:
buildscript {
  repositories {
    maven {
      url "https://plugins.gradle.org/m2/"
    }
  }
  dependencies {
    classpath "gradle.plugin.com.dorongold.plugins:task-tree:1.2.2"
  }
}
apply plugin: "com.dorongold.task-tree"

然后執(zhí)行gradle <任務(wù)名> taskTree --no-repeat,即可看到指定Task的依賴關(guān)系,比如在Java構(gòu)建中查看build任務(wù)的依賴關(guān)系:

:build
+--- :assemble
|    \--- :jar
|         \--- :classes
|              +--- :compileJava
|              \--- :processResources
\--- :check
     \--- :test
          +--- :classes *
          \--- :testClasses
               +--- :compileTestJava
               |    \--- :classes *
               \--- :processTestResources

我們看到build主要執(zhí)行了assemble包裝任務(wù)和check測試任務(wù),那么我們可以將我們自定義的cleanTemp插入到build和assemble之間。

  1. 動態(tài)插入自定義任務(wù)
    我們先定義一個自定的任務(wù)cleanTemp,讓其依賴于assemble。
task cleanTemp(dependsOn: assemble) {
  doLast {
    println '清除所有臨時文件'
  }
}

接著,我們將cleanTemp添加到build的依賴項中。

afterEvaluate {
  build.dependsOn cleanTemp
}

注意,dependsOn方法只是添加一個依賴項,并不清除之前的依賴項,所以現(xiàn)在的依賴關(guān)系如下:

:build
+--- :assemble
|    \--- :jar
|         \--- :classes
|              +--- :compileJava
|              \--- :processResources
+--- :check
|    \--- :test
|         +--- :classes
|         |    +--- :compileJava
|         |    \--- :processResources
|         \--- :testClasses
|              +--- :compileTestJava
|              |    \--- :classes
|              |         +--- :compileJava
|              |         \--- :processResources
|              \--- :processTestResources
\--- :cleanTemp
     \--- :assemble
          \--- :jar
               \--- :classes
                    +--- :compileJava
                    \--- :processResources

可以看到,cleanTemp依賴于assemble,同時build任務(wù)多了一個依賴,而build和assemble原有的依賴關(guān)系并沒有改變,執(zhí)行gradle build后任務(wù)調(diào)用結(jié)果如下:

:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar UP-TO-DATE
:assemble UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:cleanTemp
清除所有臨時文件
:build

BUILD SUCCESSFUL

結(jié)語

理解Gradle構(gòu)建的生命周期是學(xué)習(xí)Gradle構(gòu)建系統(tǒng)的基礎(chǔ),對于梳理構(gòu)建系統(tǒng)執(zhí)行流程以及編寫自己的構(gòu)建流程都是非常有幫助的,希望這篇文章能夠幫助到迷茫的初學(xué)者。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容