Gradle基礎 - 構建生命周期和Hook技術

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

構建的生命周期

任何Gradle的構建過程都分為三部分:初始化階段、配置階段和執行階段。

初始化階段

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

Settings.java

比如可以通過如下代碼向Gradle的構建過程添加監聽:

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

執行gradle build,打印結果如下:

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

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

classpath
No dependencies

BUILD SUCCESSFUL

Total time: 0.959 secs
構建結束 

配置階段

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

println 'build.gradle的配置階段'

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

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

println '我是順序執行的'

調用gradle build,得到如下結果:

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

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

classpath
No dependencies

BUILD SUCCESSFUL

Total time: 1.144 secs

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

執行階段

在配置階段結束后,Gradle會根據任務Task的依賴關系創建一個有向無環圖,可以通過Gradle對象的getTaskGraph方法訪問,對應的類為TaskExecutionGraph,然后通過調用gradle <任務名>執行對應任務。

下面我們展示如何調用子項目中的任務。

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

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

task mother << {
  println 'task mother 先于 task father 執行'
}

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

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

它們的依賴關系如下:

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

執行gradle :subproject:child,得到如下打印結果:

:subproject:mother
task mother 先于 task father 執行
:subproject:grandpa
task grandpa:doFirst 先于 doLast 執行
task grandpa:doLast
:subproject:father
task father:doLast
:subproject:child
task child 最后執行

BUILD SUCCESSFUL

Total time: 1.005 secs

因為在配置階段,我們聲明了任務mother的優先級高于任務father,所以mother先于father執行,而任務father依賴于任務grandpa,所以grandpa先于father執行。任務nobody不存在于child的依賴關系中,所以不執行。

Hook點

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

Gradle構建周期中的Hook點

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

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

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

當構建走到build.gradle時說明初始化過程已經結束了,所以上面的回調都不會執行,把上述代碼移動到settings.gradle中就正確了。

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

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

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

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

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

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

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

  • 為指定Task動態添加Action

gradle.taskGraph.beforeTask { task ->
  task << {
    println '動態添加的Action'
  }
}

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

在任務Test執行前,動態添加了一個doLast動作。

  • 獲取構建各階段耗時情況

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 '執行階段,' + task + '耗時:' + (System.currentTimeMillis() - task.beginOfTask) + 'ms'
  }
}
gradle.buildFinished {
  println '執行階段,耗時:' + (System.currentTimeMillis() - beginOfProjectExcute) + 'ms'
}

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

  • 動態改變Task依賴關系

有時我們需要在一個已有的構建系統中插入我們自己的構建任務,比如在執行Java構建后我們想要刪除構建過程中產生的臨時文件,那么我們就可以自定義一個名叫cleanTemp的任務,讓其依賴于build任務,然后調用cleanTemp任務即可。
但是這種方式適用范圍太小,比如在使用IDE執行構建時,IDE默認就是調用build任務,我們沒法修改IDE的行為,所以我們需要將自定義的任務插入到原有的任務關系中。

  1. 尋找插入點
    如果你對一個構建的任務依賴關系不熟悉的話,可以使用一個插件來查看,在根項目的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"

然后執行gradle <任務名> taskTree --no-repeat,即可看到指定Task的依賴關系,比如在Java構建中查看build任務的依賴關系:

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

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

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

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

afterEvaluate {
  build.dependsOn cleanTemp
}

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

: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任務多了一個依賴,而build和assemble原有的依賴關系并沒有改變,執行gradle build后任務調用結果如下:

: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

結語

理解Gradle構建的生命周期是學習Gradle構建系統的基礎,對于梳理構建系統執行流程以及編寫自己的構建流程都是非常有幫助的,希望這篇文章能夠幫助到迷茫的初學者。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容