為什麼說 Gradle 是 Android 進階繞不去的坎 —— Gradle 系列(1)

彭旭銳 發表於 2022-05-16
Android Gradle

請點贊,你的點贊對我意義重大,滿足下我的虛榮心。

🔥 Hi,我是小彭。本文已收錄到 GitHub · Android-NoteBook 中。這裡有 Android 進階成長知識體系,有志同道合的朋友,歡迎跟著我一起成長。(聯絡方式在 GitHub)

前言

Gradle 作為官方主推的構建系統,目前已經深度應用於 Android 的多個技術體系中,例如元件化開發、產物構建、單元測試等。可見,要成為 Android 高階工程師 Gradle 是必須掌握的知識點。在這篇文章裡,我將帶你由淺入深建立 Gradle 的基本概念,涉及 Gradle 生命週期、Project、Task 等知識點,這些內容也是 Gradle 在面試八股文中容易遇見的問題。

從這篇文章開始,我將帶你全面掌握 Gradle 構建系統,系列文章:


1. 認識 Gradle

Gradle 並不僅僅是一個語言,而是一套構建工具。在早期,軟體構建只有編譯和打包等簡單需求,但軟體開發的發展,現在的構建變得更加複雜。而構建工具就是在這一背景下衍生出來的工具鏈,它能夠幫助開發者可重複、自動化地生成目標產物。例如 Ant、Maven 和 ivy 也是歷史演化過程中誕生的構建工具。

1.1 Gradle 的優缺點

相比於早期出現的構建工具,Gradle 能夠脫穎而出主要是以下優點:

  • 表達性的 DSL: Gradle 構建指令碼採用基於 Groovy 的 DSL 領域特定語言,而不是採用傳統的 XML 檔案,相比 Maven 等構建系統更加簡潔;
  • 基於 Java 虛擬機器: Groovy 語言基於 Java 虛擬機器,這使得 Gradle 支援用 Java / Kotlin 程式碼編寫構建指令碼,我們完全可以只學習一小部分 Groovy 語法就能上手 Gradle 指令碼,降低了 Gradle 的學習強度;
  • 約定優先於配置: Gradle 具有約定優先於配置的原則,即為屬性提供預設值,相比 Ant 等構建系統更容易上手。我們在開發 Gradle 外掛時也需要遵循這一原則。

Gradle 也有明顯的缺點,例如:

  • 較弱的向後相容性: Gradle 是一個快速發展的工具,新版本經常會打破向後相容性,有經驗的同學就知道,一個工程在低版本 Gradle 可以編譯,但換了新版本 Gradle 可能就編譯不通過了。

1.2 Gradle 工程的基本結構

在 Android Studio 中建立新專案時,會自動生成以下與 Gradle 相關檔案。這些大家都很熟悉了,簡單梳理下各個檔案的作用:

.
├── a-subproject
│   └── build.gradle
├── build.gradle
├── settings.gradle
├── gradle.properties
├── local.properties
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
└── gradlew.bat
  • settings.gradle 檔案: 用於確定哪些模組參與構建;
  • 專案級 build.gradle 檔案: 用於定義所有子模組公共的配置引數;
  • 模組級 build.gradle 檔案: 用於定義子模組的配置引數,它可以覆蓋專案級 build.gradle 檔案中定義的配置;
  • gradle/warpper: 負責自動下載安裝專案所需的 Gradle 環境的指令碼;
  • gradle.properties: 用作專案級 Gradle 配置項,會覆蓋全域性的配置項;
  • local.properties: 用作專案的私有屬性配置,例如 SDK 安裝目錄,一般不把 local.properties 加入版本控制。

1.3 Gradle 中的重要概念

  • Gradle: 提供核心構建流程,但不提供具體構建邏輯;
  • Gradle 外掛: Gradle 提供的是一套核心的構建機制,而 Gradle 外掛正是執行在這套機制上的一些具體構建邏輯,本質上和 .gradle 檔案沒有區別。例如,我們熟悉的 Android 構建流程就是由 Android Gradle Plugin 引入的構建邏輯;
  • Gradle Daemon: 用於提升構建速度的後臺程式;
  • Gradle Wrapper: 對 Gradle 的封裝,增加了自動下載安裝 Gradle 環境的能力;
  • 環境變數 GRADLE: 用於定義 Gradle 的安裝目錄;
  • 環境變數 GRADLE_USER_HOME: 用於定義 Gradle 執行過程的檔案儲存目錄,例如 Gradle Wrapper 自動安裝的 Gradle 環境、構建快取等;

1.4 Gradle Daemon

Gradle Daemon 是 Gradle 3.0 引入的構建優化策略,通過規避重複建立 JVM 和記憶體快取的手段提升了構建速度。 Daemon 程式才是執行構建的程式,當構建結束後,Daemon 程式並不會立即銷燬,而是儲存在記憶體中等待承接下一次構建。根據官方文件說明,Gradle Daemon 能夠降低 15-75% 的構建時間。

Daemon 的優化效果主要體現在 3 方面:

  • 1、縮短 JVM 虛擬機器啟動時間: 不需要重複建立;
  • 2、JIT 編譯: Daemon 程式會執行 JIT 編譯,有助於提升後續構建的位元組碼執行效率;
  • 3、構建快取: 構建過程中載入的類、資源或者 Task 的輸入和輸出會儲存在記憶體中,可以被後續構建複用。

相關的 Gradle 命令:

  • gradle —status: 檢視存活的 Daemon 程式資訊;
  • gradle —stop: 停止所有 Daemon 程式。

提示: 並不是所有的構建都會複用同一個 Daemon 程式,如果已存活的 Daemon 程式無法滿足新構建的需求,則 Gradle 會新建一個新的 Daemon 程式。影響因素:

  • Gradle 版本: 不同 Gradle 版本的構建不會關聯到同一個 Daemon 程式;
  • Gradle 虛擬機器引數: 不滿足的虛擬機器引數不會關聯到同一個 Daemon 程式。

1.5 Gradle Wrapper

Gradle Wrapper 本質是對 Gradle 的一層包裝,會在執行 Gradle 構建之前自動下載安裝 Gradle 環境。 在開始執行 Gradle 構建時,如果當前裝置中還未安裝所需版本的 Gradle 環境,Gradle Wrapper 會先幫你下載安裝下來,將來其他需要這個 Gradle 版本的工程也可以直接複用。

file

Android Studio 預設使用 Gradle Wrapper 執行構建,你可以在設定中修改這一行為:

file

命令列也有區分:

  • gradle :使用系統環境變數定義的 Gradle 環境進行構建;
  • gradlew :使用 Gradle Wrapper 執行構建。

為什麼 Gradle 官方從早期就專門推出一個自動安裝環境工具呢,我認為原因有 2 個:

  • 確保 Gradle 版本正確性: 鑑於 Gradle 有較弱向後相容性的特點,Gradle Wrapper 能夠從專案工程級別固化專案所需要的 Gradle 版本,從而確保同一個工程移植到其他電腦後能夠正確地、可重複地構建;
  • 減少了手動安裝 Gradle 環境的工作量: 單單從 Gradle 4 到 Gradle 7 就有大大小小十幾個版本,而且每個工程所需要的 Gradle 版本不盡相同,使用 Gradle Wrapper 能夠減少手動安裝環境的工作量;

簡單說下 Gradle Wrapper 相關的檔案,主要有 4 個:

  • gradlew & gradlew.bat: 在 Linux 或 Mac 上可用的 Shell 指令碼,以及在 Window 上可用的 Batch 指令碼,用於以 Gradle Wrapper 的方式執行構建。也就是說,在命令列使用 gradlew 才是基於 Gradle Wrapper 執行的,而使用 gradle 命令是直接基於系統安裝的 Gradle 環境執行編譯;
  • gradle-wrapper.jar: 負責下載安裝 Gradle 環境的指令碼;
  • gradle-wrapper.properties: Gradle Wrapper 的配置檔案,主要作用是決定 Gradle 版本和安裝目錄:
    • distributionBase + distributionPath:指定 Gradle 環境安裝路徑;
    • zipStoreBase + zipStorePath:指定 Gradle 安裝包的儲存路徑;
    • distributionUrl:指定版本 Gradle 的下載地址,通過這個引數可以配置專案工程所需要的 Gradle 版本。
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

提示: GRADLE_USER_HOME 的預設值是 使用者目錄/.gradle,可以通過系統環境變數 GRADLE_USER_HOME 修改。

1.6 gradle.properties 構建環境配置

Gradle 是執行在 Java 虛擬機器的,gradle.properties 檔案可以配置 Gradle 構建的執行環境,並且會覆蓋 Android Studio 設定中的全域性配置,完整構建環境配置見官方文件:Build Enviroment。常用的配置項舉例:

# Gradle Daemon 開關,預設 ture
org.gradle.daemon=true  

# 虛擬機器引數
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8

# 多模組工程並行編譯多個模組,會消耗更多記憶體
org.gradle.parallel=true  

除了構建環境配置,其他配置也可以用類似的鍵值對方式放在 gradle.properties 中,並直接在 .gradle 檔案中引用。


2. Groovy 必知必會

Groovy 是從 Java 虛擬機器衍生出來的語言,由於我們都具備一定的 Java 基礎,所以我們沒有必要完全從零開始學習 Groovy。梳理 Groovy 與 Java 之間有差異的地方,或許是更高效的學習方式:

2.1 一些小差異

  • 分號: 語句允許不以分號 ; 結尾;
  • public: 預設的訪問修飾符為 public;
  • getter / setter: Groovy 會為每個 field 建立對應的 getter / setter 方法,在訪問 obj.field / obj.field=”” 時,實際上是在訪問 getField() 和 setField(””);
  • 支援靜態型別和動態型別: Groovy 既支援 Java 的靜態型別,也支援通過 def 關鍵字宣告動態型別(靜態型別和動態型別的關鍵區別在於 ”型別檢查是否傾向於在編譯時執行“。例如 Java 是靜態型別語言,意味著型別檢查主要由編譯器在編譯時完成);
  • 字串: Groovy 支援三種格式定義字串 —— 單引號、雙引號和三引號
    • 單引號:純粹的字串,與 Java 的雙引號字串類似;
    • 雙引號:支援在引號內通過 $ 關鍵字直接引用變數值;
    • 三引號:支援換行。

2.2 函式

  • 函式定義: Groovy 支援通過返回型別或 def 關鍵字定義函式。def 關鍵字定義的函式如果沒有 return 關鍵字返回值,則預設會返回 null。例如:
// 使用 def 關鍵字
def methodName() {
    // Method Code
}

String methodName() {
    // Method Code
}
  • 引數名: Groovy 支援不指定引數型別。例如:
// 省略引數型別
def methodName(param1, param2) {
    // Method Code
}

def methodName(String param1, String param2) {
    // Method Code
}
  • 預設引數: Groovy 支援指定函式引數預設值,預設引數必須放在引數列表末尾。例如:
def methodName(param1, param2 = 1) {
    // Method Code
}
  • 返回值: 可以省略 return,預設返回最後一行語句的值。例如:
def methodName() {
    return "返回值"
}
等價於
def methodName() {
    "返回值"
}
  • invokeMethod & methodMissing:
    • invokeMethod: 分派物件上所有方法呼叫,包括已定義和未定義的方法,需要實現 GroovyInterceptable 介面;
    • methodMissing: 分派物件上所有為定義方法的呼叫。
// 實現 GroovyInterceptable 介面,才會把方法呼叫分派到 invokeMethod。
class Student implements GroovyInterceptable{
    def name;

    def hello() {
        println "Hello ${name}"
    }

    @Override
    Object invokeMethod(String name, Object args) {
        System.out.println "invokeMethod : $name"
    }
}

def student = new Student(name: "Tom")

student.hello()
student.hello1()

輸出:
invokeMethod : hello
invokeMethod : hello1

-------------------------------------------------------------

class Student {
    def name;

    def hello() {
        println "Hello ${name}"
    }

    @Override
    Object methodMissing(String name, Object args) {
        System.out.println "methodMissing : $name"
    }
}

def student = new Student(name: "Tom")

student.hello()
student.hello1()

輸出:
Hello Tom
methodMissing hello1

2.3 集合

Groovy 支援通過 [] 關鍵字定義 List 列表或 Map 集合:

  • 列表: 例如 def list = [1, 2, 3, 4]
  • 集合: 例如 def map = [’name’:’Tom’, ‘age’:18],空集合 [:]
  • 範圍: 例如 def range = 1 .. 10
  • 遍歷:
// 列表
def list = [10, 11, 12]
list.each { value ->
}
list.eachWIthIndex { value, index ->
}

// 集合
def map = [’name’:’Tom’, ‘age’:18]
map.each { key, value ->
}
map.eachWithIndex { entry, index ->
}
map.eachWithIndex { key, value, index ->
}

2.4 閉包

Groovy 閉包是一個匿名程式碼塊,可以作為值傳遞給變數或函式引數,也可以接收引數和提供返回值,形式上與 Java / Kotlin 的 lambda 表示式類似。例如以下是有效的閉包:

{ 123 }                                          

{ -> 123 }                                       

{ println it }

{ it -> println it }

{ name -> println name }                            

{ String x, int y ->                                
    println "hey ${x} the value is ${y}"
}
  • 閉包型別: Groovy 將閉包定義為 groovy.lang.Closure 的例項,使得閉包可以像其他型別的值一樣複製給變數。例如:
Closure c = { 123 }

// 當然也可以用 def 關鍵字
def c = { 123 }
  • 閉包呼叫: 閉包可以像方法一樣被呼叫,可以通過 Closure#call() 完成,也可以直接通過變數完成。例如:
def c = { 123 }

// 通過 Closure#call() 呼叫
c.call()

// 直接通過變數名呼叫
c()
  • 隱式引數: 閉包預設至少有一個形式引數,如果閉包沒有顯式定義引數列表(使用 ),Groovy 總是帶有隱式新增一個引數 it。如果呼叫者沒有使用任何實參,則 it 為空。當你需要宣告一個不接收任何引數的閉包,那麼必須用顯式的空引數列表宣告。例如:
// 帶隱式引數 it
def greeting = { "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'

// 不帶隱式引數 it
def magicNumber = { -> 42 }
// error 不允許傳遞引數
magicNumber(11)
  • 閉包引數簡化: 函式的最後一個引數是閉包型別的化,在呼叫時可以簡化,省略圓括號:
def methodName(String param1, Closure closure) {
    // Method Code
}

// 呼叫:
methodName("Hello") {
    // Closure Code
}
  • this、owner、delegate: 閉包委託是 Groovy Closure 相比 Java Lambda 最大的區別,通過修改閉包的委託可以實現靈活多樣的 DSL。先認識閉包中的三個變數:
    • this: 定義閉包的外部類,this 一定指向類物件;
    • owner: 定義閉包的外部物件,owner 可能是類物件,也可能是更外一層的閉包;
    • delegate: 預設情況 delegate 等同於 owner,this 和 owner 的語義無法修改,而 delegate 可以修改。
  • 閉包委託策略: 在閉包中,如果一個屬性沒有顯式宣告接收者物件,則會通過閉包代理解析策略尋找定義的物件,例如:
class Person {
    String name
}
def p = new Person(name:'Igor')
def cl = { 
    // 相當於 delegate.name.toUpperCase()
    name.toUpperCase() 
}                 
cl.delegate = p                                 
assert cl() == 'IGOR'

閉包定義了多種解析策略,可以通過 Closure#resolveStrategy=Closure.DELEGATE_FIRST 修改:

  • Closure.OWNER_FIRST(預設): 優先在 owner 物件中尋找,再去 delegate 物件中尋找;
  • Closure.DELEGATE_FIRST: 優先在 delegate 物件中尋找,再去 owner 物件中尋找;
  • Closure.OWNER_ONLY: 只在 owner 物件中尋找;
  • Closure.DELEGATE_ONLY: 只在 delegate 物件中尋找;
  • Closure.TO_SELF: 只在閉包本身尋找;

3. Gradle 構建生命週期

Gradle 將構建劃分為三個階段: 初始化 - 配置 - 執行 。理解構建生命週期(Gradle Build Lifecycle)非常重要,否則你可能連指令碼中的每個程式碼單元的執行時機都搞不清楚。

3.1 初始化階段

由於 Gradle 支援單模組構建或多模組構建,因此在初始化階段(Initialization Phase),Gradle 需要知道哪些模組將參與構建。主要包含 4 步:

  • 1、執行 Init 指令碼: Initialization Scripts 會在構建最開始執行,一般用於設定全域性屬性、宣告週期監聽、日誌列印等。Gradle 支援多種配置 Init 指令碼的方法,以下方式配置的所有 Init 指令碼都會被執行:
    • gradle 命令列指定的檔案:gradle —init-script <file>
    • USER_HOME/.gradle/init.gradle 檔案
    • USER_HOME/.gradle/init.d/ 資料夾下的 .gradle 檔案
    • GRADLE_HOME/init.d/ 資料夾下的 .gradle 檔案
  • 2、例項化 Settings 介面例項: 解析根目錄下的 settings.gradle 檔案,並例項化一個 Settings 介面例項;
  • 3、執行 settings.gradle 指令碼: 在 settings.gradle 檔案中的程式碼會在初始化階段執行;
  • 4、例項化 Project 介面例項: Gradle 會解析 include 宣告的模組,併為每個模組 build.gradle 檔案例項化 Project 介面例項。Gradle 預設會在工程根目錄下尋找 include 包含的專案,如果你想包含其他工程目錄下的專案,可以這樣配置:
// 引用當前工程目錄下的模組
include ':app'

// 引用其他工程目錄下的模組
include 'video' // 易錯點:不要加’冒號 :‘
project(:video).projectDir = new File("..\\libs\\video")

提示: 模組 build.gradle 檔案的執行順序和 include 順序沒有關係。

3.2 配置階段

配置階段(Configuration Phase)將執行 build.gradle 中的構建邏輯,以完成 Project 的配置。主要包含 3 步:

  • 1、下載外掛和依賴: Project 通常需要依賴其他外掛或 Project 來完成工作,如果有需要先下載;
  • 2、執行指令碼程式碼: 在 build.gradle 檔案中的程式碼會在配置階段執行;
  • 3、構造 Task DAG: 根據 Task 的依賴關係構造一個有向無環圖,以便在執行階段按照依賴關係執行 Task。

提示: 執行任何 Gradle 構建命令,都會先執行初始化階段和配置階段。

3.3 執行階段

在配置階段已經構造了 Task DAG,執行階段(Execution Phase)就是按照依賴關係執行 Task。這裡有兩個容易理解錯誤的地方:

  • 1、Task 配置程式碼在配置階段執行,而 Task 動作在執行階段執行;
  • 2、即使執行一個 Task,整個工程的初始化階段和所有 Project 的配置階段也都會執行,這是為了支援執行過程中訪問構建模型的任何部分。

原文: This means that when a single task, from a single project is requested, all projects of a multi-project build are configured first. The reason every project needs to be configured is to support the flexibility of accessing and changing any part of the Gradle project model.

介紹完三個生命週期階段後,你可以通過以下 Demo 體會各個程式碼單元所處的執行階段:

USER_HOME/.gradle/init.gradle

println 'init.gradle:This is executed during the initialization phase.'

settings.gradle

rootProject.name = 'basic'
println 'settings.gradle:This is executed during the initialization phase.'

build.gradle

println 'build.gradle:This is executed during the configuration phase.'

tasks.register('test') {
    doFirst {
        println 'build.gradle:This is executed first during the execution phase.'
    }
    doLast {
        println 'build.gradle:This is executed last during the execution phase.'
    }
    // 易錯點:這裡在配置階段執行
    println 'build.gradle:This is executed during the configuration phase as well.'
}

輸出:

Executing tasks: [test] in project /Users/pengxurui/workspace/public/EasyUpload

init.gradle:This is executed during the initialization phase.
settings.gradle:This is executed during the initialization phase.

> Configure project :
build.gradle:This is executed during the configuration phase.
build.gradle:This is executed during the configuration phase as well.

> Task :test
build.gradle:This is executed first during the execution phase.
build.gradle:This is executed last during the execution phase.

...

提示: Task 在執行階段執行有一個特例,即通過 Project#defaultTasks 指定預設任務,會在配置階段會執行,見 第 6.2 節 ,瞭解即可。

3.4 生命週期監聽

Gradle 提供了一系列監聽構建生命週期流程的介面,大部分的節點都有直接的 Hook 點,這裡我總結一些常用的:

  • 1、監聽初始化階段

Gradle 介面提供了監聽 Settings 初始化階段的方法:

settings.gradle

// Settings 配置完畢
gradle.settingsEvaluated {
    ...
}

// 所有 Project 物件建立(注意:此時 build.gradle 中的配置程式碼還未執行)
gradle.projectsLoaded {
    ...
}
  • 2、監聽配置階段

Project 介面提供了監聽當前 Project 配置階段執行的方法,其中 afterEvaluate 常用於在 Project 配置完成後繼續增加額外的配置,例如 Hook 構建過程中的 Task。

// 執行 build.gradle 前
project.beforeEvaluate { 
    ...
}

// 執行 build.gradle 後
project.afterEvaluate { 
    ...
}

除此之外,Gradle 介面也提供了配置階段的監聽:

// 執行 build.gradle 前
gradle.beforeProject { project ->
    ...
}

// 執行 build.gradle 後
gradle.afterProject { project ->
    // 配置後,無論成功或失敗
    if (project.state.failure) {
        println "Evaluation of $project FAILED"
    } else {
        println "Evaluation of $project succeeded"
    }
}

// 與 project.beforeEvaluate 和 project.afterEvaluate 等價
gradle.addProjectEvaluationListener(new ProjectEvaluationListener() {
    @Override
    void beforeEvaluate(Project project) {
        ...
    }

    @Override
    void afterEvaluate(Project project, ProjectState projectState) {
        ...
    }
})

// 依賴關係解析完畢
gradle.addListener(new DependencyResolutionListener() {
    @Override
    void beforeResolve(ResolvableDependencies dependencies) {
        ....
    }

    @Override
    void afterResolve(ResolvableDependencies dependencies) {
        ....
    }
})

// Task DAG 構造完畢
gradle.taskGraph.whenReady {   
}

// 與 gradle.taskGraph.whenReady 等價
gradle.addListener(new TaskExecutionGraphListener() {
    @Override
    void graphPopulated(TaskExecutionGraph graph) {
        ...
    }
})

// 所有 Project 的 build.gradle 執行完畢
gradle.projectsEvaluated {
    ...
}
  • 3、監聽執行階段

Gradle 介面提供了執行階段的監聽:

gradle.addListener(new TaskExecutionListener() {

    // 執行 Task 前
    @Override
    void beforeExecute(Task task) {
        ...
    }

    // 執行 Task 後
    @Override
    void afterExecute(Task task, TaskState state) {
        ...
    }
})

gradle.addListener(new TaskActionListener() {

    // 開始執行 Action 列表前,回撥時機略晚於 TaskExecutionListener#beforeExecute
    @Override
    void beforeActions(Task task) {
        ...
    }

    // 執行 Action 列表完畢,回撥時機略早於 TaskExecutionListener#afterExecute
    @Override
    void afterActions(Task task) {
        ...
    }
})

// 執行 Task 前
gradle.taskGraph.beforeTask { Task task ->
}

// 執行 Task 後
gradle.taskGraph.afterTask { Task task, TaskState state ->
    if (state.failure) {
        println "FAILED"
    }
    else {
        println "done"
    }
}
  • 4、監聽 Task 建立

TaskContainer 介面提供了監聽 Task 新增的方法,可以在 Task 新增到 Project 時收到回撥:

tasks.whenTaskAdded { task ->
}
  • 5、監聽構建結束

當所有 Task 執行完畢,意味著構建結束:

gradle.buildFinished {
    ...
}

4. Project 核心 API

Project 可以理解為模組的構建管理器,在初始化階段,Gradle 會為每個模組的 build.gradle 檔案例項化一個介面物件。在 .gradle 指令碼中編寫的程式碼,本質上可以理解為是在一個 Project 子類中編寫的。

4.1 Project API

Project 提供了一系列操作 Project 物件的 API:

  • getProject(): 返回當前 Project;
  • getParent(): 返回父 Project,如果在工程 RootProject 中呼叫,則會返回 null;
  • getRootProject(): 返回工程 RootProject;
  • getAllprojects(): 返回一個 Project Set 集合,包含當前 Project 與所有子 Project;
  • getSubprojects(): 返回一個 Project Set 集合,包含所有子 Project;
  • project(String): 返回指定 Project,不存在時丟擲 UnKnownProjectException;
  • findProject(String): 返回指定 Project,不存在時返回 null;
  • allprojects(Closure): 為當前 Project 以及所有子 Project 增加配置;
  • subprojects(Closure): 為所有子 Project 增加配置。

4.2 Project 屬性 API

Project 提供了一系列操作屬性的 API,通過屬性 API 可以實現在 Project 之間共享配置引數:

  • hasProperty(String): 判斷是否存在指定屬性名;
  • property(Stirng): 獲取屬性值,如果屬性不存在則丟擲 MissingPropertyException;
  • findProperty(String): 獲取屬性值,如果屬性不存在則返回 null;
  • setProperty(String, Object): 設定屬性值,如果屬性不存在則丟擲 MissingPropertyException。

實際上,你不一定需要顯示呼叫這些 API,當我們直接使用屬性名時,Gradle 會幫我們隱式呼叫 property() 或 setProperty()。例如:

build.gradle

name => 相當於 project.getProperty("name")
project.name = "Peng" => 相當於 project.setProperty("name", "Peng")

4.2.1 屬性匹配優先順序

Project 屬性的概念比我們理解的欄位概念要複雜些,不僅僅是一個簡單的鍵值對。Project 定義了 4 種名稱空間(scopes)的屬性 —— 自有屬性、Extension 屬性、ext 屬性、Task。 當我們通過訪問屬性時,會按照這個優先順序順序搜尋。

getProperty() 的搜尋過程:

  • 1、自有屬性: Project 物件自身持有的屬性,例如 rootProject 屬性;
  • 2、Extension 屬性;
  • 3、ext 屬性;
  • 4、Task: 新增到 Project 上的 Task 也支援通過屬性 API 訪問;
  • 5、父 Project 的 ext 屬性: 會被子 Project 繼承,因此當 1 ~ 5 未命中時,會繼續從父 Project 搜尋。需要注意: 從父 Project 繼承的屬性是隻讀的;
  • 6、以上未命中,丟擲 MissingPropertyException 或返回 null。

setProperty() 的搜尋路徑(由於部分屬性是隻讀的,搜尋路徑較短):

  • 1、自有屬性
  • 2、ext 額外屬性

提示: 其實還有 Convention 名稱空間,不過已經過時了,我們不考慮。

4.2.2 Extension 擴充套件

Extension 擴充套件是外掛為外部構建指令碼提供的配置項,用於支援外部自定義外掛的工作方式,其實就是一個對外開放的 Java Bean 或 Groovy Bean。例如,我們熟悉的 android{} 就是 Android Gradle Plugin 提供的擴充套件。

關於外掛 Extension 擴充套件的更多內容,見下一篇文章。

4.2.3 ext 屬性

Gradle 為 Project 和 Task 提供了 ext 名稱空間,用於定義額外屬性。如前所述,子 Project 會繼承 父 Project 定義的 ext 屬性,但是隻讀的。我們經常會在 Root Project 中定義 ext 屬性,而在子 Project 中可以直接複用屬性值,例如:

專案 build.gradle

ext {
    kotlin_version = '1.4.31'
}

模組 build.gradle

// 如果子 Project 也定義了 kotlin_version 屬性,則不會引用父 Project
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

4.3 Project 檔案 API

4.3.1 檔案路徑

  • getRootDir(): Project 的根目錄(不是工程根目錄)
  • getProjectDir(): 包含 build 資料夾的專案目錄
  • getBuildDir(): build 資料夾目錄

4.3.2 檔案獲取

  • File file(Object path): 獲取單個檔案,相對位置從當前 Project 目錄開始
  • ConfigurableFileCollection files(Object... paths): 獲取多個檔案,相對位置從當前 Project 目錄開始
def destFile = file('releases.xml')
if (destFile != null && !destFile.exists()) {
    destFile.createNewFile()
}

4.3.3 檔案拷貝

  • copy(Closure): 檔案拷貝,引數閉包用於配置 CodeSpec 物件
copy {
    // 來原始檔
    from file("build/outputs/apk")
    // 目標檔案
    into getRootProject().getBuildDir().path + "/apk/"
    exclude {
        // 排除不需要拷貝的檔案
    }
    rename {
        // 對拷貝過來的檔案進行重新命名
    }
}

4.3.4 檔案遍歷

  • fileTree(Object baseDir): 將指定目錄轉化為檔案樹,再進行遍歷操作
fileTree("build/outputs/apk") { FileTree fileTree ->
    fileTree.visit { FileTreeElement fileTreeElement ->
        // 檔案操作
    }
}

5. Task 核心 API

Project 的構建邏輯由一系列 Task 的組成,每個 Task 負責完成一個基本的工作,例如 Javac 編譯 Task、資源編譯 Task、Lint 檢查 Task,簽名 Task等。在構建配置階段,Gradle 會根據 Task 的依賴關係構造一個有向無環圖,以便在執行階段按照依賴關係執行 Task。

5.1 建立簡單 Task

Gradle 支援兩種建立簡單 Task 的語法:

  • 1、通過 task 關鍵字:
// 建立名為 MyTask 的任務
task MyTask(group: "MyGroup") {
    // Task 配置程式碼
}
  • 2、通過 TaskContainer 方法: 通過 Project 的 TaskContainer 屬性,可以建立 Task,分為熱建立和懶建立:
    • Task create(String, Closure) 熱建立: 立即例項化 Task 物件;
    • TaskProvider register(String, Closure) 懶建立: 註冊 Task 構造器,但不會例項化物件。建立 Task 操作會延遲到訪問該 Task 時,例如通過 TaskProvider#get() 或 TaskContainer#getByName()。
// 建立名為 MyTask 的任務
project.tasks.create(name: "MyTask") {
    // Task 配置程式碼
}

5.2 建立增強 Task(自定義 Task 型別)

除了簡單建立 Task 的方式,我們還可以自定義 Task 型別,Gradle 將這類 Task 稱為增強 Task。增強 Task 的可重用性更好,並且可以通過暴露屬性的方式來定製 Task 的行為。

  • 1、DefaultTask: 自定義 Task 必須繼承 DefaultTask。
class CustomTask extends DefaultTask {
    final String message
    final int number
}
  • 2、帶引數建立 Task: 除了可以在建立 Task 後配置屬性值,我們也可以在呼叫 TaskContainer#create() 時傳遞構造器引數。為了將值傳遞給任務建構函式,必須使用 @Inject 註解修飾構造器。
class CustomTask extends DefaultTask {
    final String message
    final int number

    @Inject
    CustomTask(String message, int number) {
        this.message = message
        this.number = number
    }
}
// 第二個引數為 Task 型別
tasks.register('myTask', CustomTask, 'hello', 42)

5.3 獲取已建立 Task

可以獲取 TaskContainer 中已建立的任務,對於通過 register 註冊的任務會在這個時機例項化。例如:

  • Task getByName(String): 獲取 Task,如果 Task 不存在則丟擲 UnKnownDomainObjectException;
  • Task findByName(String): 獲取 Task,如果 Task 不存在則返回 null。
// 獲取已建立的 Task
project.MyTask.name => 等同於 project.tasks.getByName("MyTask").name

5.4 設定 Task 屬性

設定 Task 屬性的語法主要有三種:

  • 1、在建立 Task 時設定
task MyTask(group: "MyGroup")
  • 2、通過 setter 方法設定
task MyTask {
    group = "MyGroup" => 等同於 setGroup("MyGroup")
}
  • 3、通過 ext 額外屬性設定: Task 也支援與 Project 類似的額外屬性。例如:
task MyTask(group:"111") {
    ext.goods = 2
}

ext.goods = 1

println MyTask.good

輸出:2

Task 常用的自有屬性如下:

屬性 描述
name Task 識別符號,在定義 Task 時指定
group Task 所屬的組
description Task 的描述資訊
type Task型別,預設為 DefaultTask
actions 動作列表
dependsOn 依賴列表

注意事項:

  • 嚴格避免使用帶空格的 Task name,否則在一些版本的 Android Studio 中會被截斷,導致不相容;
  • Android Studio 的 Gradle 皮膚會按照 group 屬性對 Task 進行分組顯示。其中, Tasks 組為 Root Project 中的 Task,其他分組為各個 Project 中的 Task,未指定 group 的 Task 會分配在 other 中。

file

5.5 執行 Task

  • 1、命令列: gradlew :[模組名]:[任務名],例如:gradlew -q :app:dependencies
  • 2、IDE 工具: 通過 IDE 提供的使用者介面工具執行,例如 Gradle 皮膚或綠色三角形,支援普通執行和除錯執行;
  • 3、預設任務: 通過 Project#defaultTasks 可以指定 Project 配置階段的預設任務,在配置階段會執行(這說明 Task 是有可能在配置階段執行的,瞭解即可,不用鑽牛角尖)。

build.gradle

defaultTasks 'hello','hello2'

task hello {
    println "defaultTasks hello"
}

task hello2 {
    println "defaultTasks hello2"
}

輸出:
> Configure project :easyupload
defaultTasks hello
defaultTasks hello2
--afterEvaluate--
--taskGraph.whenReady--

5.6 Task Action 動作

每個 Task 內部都保持了一個 Action 列表 actions,執行 Task 就是按順序執行這個列表,Action 是比 Task 更細的程式碼單元。Task 支援新增多個動作,Task 提供了兩個方法來新增 Action:

  • doFirst(Closure): 在 Action 列表頭部新增一個 Action;
  • doLast(Closure): 在 Action 列表尾部新增一個 Action。
task MyTask

MyTask.doFirst{
    println "Action doFirst 1"
}

MyTask.doFirst{
    println "Action doFirst 2"
}

MyTask.doLast{
    println "Action doLast 1"
}

執行 MyTask 輸出:

Action doFirst 2
Action doFirst 1
Action doLast 1

對於自定義 Task,還可以通過 @TaskAction 註解新增預設 Action。例如:

abstract class CustomTask extends DefaultTask {
    @TaskAction
    def greet() {
        println 'hello from GreetingTask'
    }
}

5.7 跳過 Task 的執行

並不是所有 Task 都會被執行,Gradle 提供了多個方法來控制跳過 Task 的執行:

  • 1、onlyIf{}: 閉包會在即將執行 Task 之前執行,閉包返回值決定了是否執行 Task;
  • 2、enabled 屬性: Task 的 enabled 屬性預設為 true,設定為 false 表示無效任務,不需要執行。

剩下兩種方式允許在執行 Task 的過程中中斷執行:

  • 3、Task 異常: Task 提供了兩個異常,能夠當 Action 執行過程中丟擲以下異常,將跳過執行並繼續後續的構建過程:
  • 4、timeouts 屬性: 當 Task 執行時間到達 timeouts 超時時間時,執行執行緒會收到一箇中斷訊號,可以藉此許控制 Task 的執行時間(前提是 Task 要響應中斷訊號)。

5.8 Task 依賴關係

通過建立 Task 的依賴關係可以構建完成的 Task 有向無環圖:

  • dependsOn 強依賴: Task 通過 dependsOn 屬性建立強依賴關係,可以直接通過 dependsOn 屬性設定依賴列表,也可以通過 dependsOn() 方法新增一個依賴;
  • 輸入輸出隱式依賴: 通過建立 Task 之間的輸入和輸出關係,也會隱式建立依賴關係。例如 Transform Task 之間就是通過輸入輸出建立的依賴關係。

// 通過屬性設定依賴列表
task task3(dependsOn: [task1, task2]) {
}

// 新增依賴
task3.dependsOn(task1, task2)

依賴關係:task3 依賴於 [task1, task2],在執行 task3 前一定會執行 task1 和 task2

在某些情況下,控制兩個任務的執行順序非常有用,而不會在這些任務之間引入顯式依賴關係,可以理解為弱依賴。 任務排序和任務依賴關係之間的主要區別在於,排序規則不影響將執行哪些任務,隻影響任務的執行順序。

  • mustRunAfter 強制順序: 指定強制要求的任務執行順序;
  • shouldRunAfter 非強制順序: 指定非強制的任務執行順序,在兩種情況下會放棄此規則:1、該規則造成環形順序;2、並行執行並且任務的所有依賴項都已經完成。
task3 mustRunAfter(task1, task2)
task3 shouldRunAfter(task1, task2)

依賴關係:無,在執行 task3 前不一定會執行 task1 和 task2
順序關係:[task1, task2] 優先於 task3

5.9 Finalizer Task

給一個 Task 新增 Finalizer 終結器任務後,無論 Task 執行成功還是執行失敗,都會執行終結器,這對於需要在 Task 執行完畢後清理資源的情況非常有用。

// taskY 是 taskX 的終結器
taskX finalizedBy taskY

6. 增量構建

6.1 什麼是增量構建?

任何構建工具都會盡量避免重複執行相同工作,這一特性稱為 Incremental Build 增量構建,這一特效能夠節省大量構建時間。例如編譯過原始檔後就不應該重複編譯,除非發生了影響輸出的更改(例如修改或刪除原始檔)。

Gradle 通過對比自從上一次構建之後,Task 的 inputsoutputs 是否變化,來決定是否跳過執行。如果相同,則 Gralde 認為 Task 是最新的,從而會跳過執行。在 Build Outputs 中看到 Task 名稱旁邊出現 UP-TO-DATE 標誌,即說明該 Task 是被跳過的。例如:

> Task :easyupload:compileJava NO-SOURCE
> Task :easyupload:compileGroovy UP-TO-DATE
> Task :easyupload:pluginDescriptors UP-TO-DATE
> Task :easyupload:processResources UP-TO-DATE
> Task :easyupload:classes UP-TO-DATE
> Task :easyupload:jar UP-TO-DATE
> Task :easyupload:uploadArchives

那麼,在定義 Task 的輸入輸出時,要遵循一個原則:如果 Task 的一個屬性會影響輸出,那麼應該將該屬性註冊為輸入,否則會影響 Task 執行;相反,如果 Task 的一個屬性不會影響輸出,那麼不應該將該屬性註冊為輸入,否則 Task 會在不必要時執行。

6.2 Task 輸入輸出

大多數情況下,Task 需要接收一些 input 輸入,並生成一些 output 輸出。例如編譯任務,輸入是原始檔,而輸出是 Class 檔案。Task 使用 TaskInputsTaskOutputs 管理輸入輸出:

  • Task#inputs: 返回 Task 的 TaskInputs 輸入管理器;
  • Task#outputs: 返回 Task 的 TaskOutputs 輸出管理器。

file

對於 Task 的輸入輸出,我們用物件導向的概念去理解是沒問題的。如果我們把 Task 理解為一個函式,則 Task 的輸入就是函式的引數,而 Task 的輸出就是函式的返回值。在此理解的基礎上,再記住 2 個關鍵點:

  • 1、隱式依賴: 如果一個 Task 的輸入是另一個 Task 的輸出,Gradle 會推斷出兩者之間的強依賴關係;
  • 2、在配置階段宣告: 由於 Task 的輸入輸出會用於構建依賴關係,那麼我們應該確保在配置階段定義輸入輸出,而不是在執行階段定義。

Task 支援三種形式的輸入:

  • 1、簡單值: 包括數值、字串和任何實現 Serializable 的類;
  • 2、檔案: 包括單個檔案或檔案目錄;
  • 3、巢狀物件: 不滿足以上兩種條件,但其欄位宣告為輸入。
public abstract class ProcessTemplates extends DefaultTask {

    @Input
    public abstract Property<TemplateEngineType> getTemplateEngine();

    @InputFiles
    public abstract ConfigurableFileCollection getSourceFiles();

    @Nested
    public abstract TemplateData getTemplateData();

    @OutputDirectory
    public abstract DirectoryProperty getOutputDir();

    @TaskAction
    public void processTemplates() {
        // ...
    }
}

public abstract class TemplateData {

    @Input
    public abstract Property<String> getName();

    @Input
    public abstract MapProperty<String, String> getVariables();
}

6.3 Task 輸入輸出校驗

通過註解方式註冊輸入輸出時,Gradle 會在配置階段會對屬性值進行檢查。如果屬性值不滿足條件,則 Gradle 會丟擲 TaskValidationException 異常。特殊情況時,如果允許輸入為 null 值,可以新增 @Optional 註解表示輸入可空。

  • @InputFile: 驗證該屬性值不為 null,並且關聯一個檔案(而不是資料夾),且該檔案存在;
  • @InputDirectory: 驗證該屬性值不為 null,並且關聯一個資料夾(而不是檔案),且該資料夾存在;
  • @OutputDirectory: 驗證該屬性值不為 null,並且關聯一個資料夾(而不是檔案),當該資料夾不存在時會建立該資料夾。

7. 總結

到這裡,Gradle 基礎的部分就講完了,下一篇文章我們來討論 Gradle 外掛。提個問題,你知道 Gradle 外掛和 .gradle 檔案有區別嗎?關注我,帶你瞭解更多。


參考資料