為什么說 Gradle 是 Android 進(jìn)階繞不去的坎 —— Gradle 系列(1)

請(qǐng)點(diǎn)贊琅摩,你的點(diǎn)贊對(duì)我意義重大晨汹,滿足下我的虛榮心终议。

?? Hi虫埂,我是小彭祥山。本文已收錄到 GitHub · Android-NoteBook 中。這里有 Android 進(jìn)階成長知識(shí)體系掉伏,有志同道合的朋友缝呕,歡迎跟著我一起成長。(聯(lián)系方式在 GitHub)

前言

Gradle 作為官方主推的構(gòu)建系統(tǒng)斧散,目前已經(jīng)深度應(yīng)用于 Android 的多個(gè)技術(shù)體系中供常,例如組件化開發(fā)、產(chǎn)物構(gòu)建鸡捐、單元測試等栈暇。可見箍镜,要成為 Android 高級(jí)工程師 Gradle 是必須掌握的知識(shí)點(diǎn)源祈。在這篇文章里煎源,我將帶你由淺入深建立 Gradle 的基本概念,涉及 Gradle 生命周期香缺、Project手销、Task 等知識(shí)點(diǎn),這些內(nèi)容也是 Gradle 在面試八股文中容易遇見的問題图张。

從這篇文章開始原献,我將帶你全面掌握 Gradle 構(gòu)建系統(tǒng),系列文章:


1. 認(rèn)識(shí) Gradle

Gradle 并不僅僅是一個(gè)語言讲仰,而是一套構(gòu)建工具。在早期痪蝇,軟件構(gòu)建只有編譯和打包等簡單需求鄙陡,但軟件開發(fā)的發(fā)展,現(xiàn)在的構(gòu)建變得更加復(fù)雜躏啰。而構(gòu)建工具就是在這一背景下衍生出來的工具鏈趁矾,它能夠幫助開發(fā)者可重復(fù)、自動(dòng)化地生成目標(biāo)產(chǎn)物给僵。例如 Ant毫捣、Maven 和 ivy 也是歷史演化過程中誕生的構(gòu)建工具。

1.1 Gradle 的優(yōu)缺點(diǎn)

相比于早期出現(xiàn)的構(gòu)建工具帝际,Gradle 能夠脫穎而出主要是以下優(yōu)點(diǎn):

  • 表達(dá)性的 DSL: Gradle 構(gòu)建腳本采用基于 Groovy 的 DSL 領(lǐng)域特定語言蔓同,而不是采用傳統(tǒng)的 XML 文件,相比 Maven 等構(gòu)建系統(tǒng)更加簡潔蹲诀;
  • 基于 Java 虛擬機(jī): Groovy 語言基于 Java 虛擬機(jī)斑粱,這使得 Gradle 支持用 Java / Kotlin 代碼編寫構(gòu)建腳本,我們完全可以只學(xué)習(xí)一小部分 Groovy 語法就能上手 Gradle 腳本脯爪,降低了 Gradle 的學(xué)習(xí)強(qiáng)度则北;
  • 約定優(yōu)先于配置: Gradle 具有約定優(yōu)先于配置的原則蚯姆,即為屬性提供默認(rèn)值砚哆,相比 Ant 等構(gòu)建系統(tǒng)更容易上手发乔。我們在開發(fā) Gradle 插件時(shí)也需要遵循這一原則罐氨。

Gradle 也有明顯的缺點(diǎn),例如:

  • 較弱的向后兼容性: Gradle 是一個(gè)快速發(fā)展的工具隅忿,新版本經(jīng)常會(huì)打破向后兼容性迫靖,有經(jīng)驗(yàn)的同學(xué)就知道,一個(gè)工程在低版本 Gradle 可以編譯拇泛,但換了新版本 Gradle 可能就編譯不通過了滨巴。

1.2 Gradle 工程的基本結(jié)構(gòu)

在 Android Studio 中創(chuàng)建新項(xiàng)目時(shí),會(huì)自動(dòng)生成以下與 Gradle 相關(guān)文件俺叭。這些大家都很熟悉了恭取,簡單梳理下各個(gè)文件的作用:

.
├── 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 文件: 用于確定哪些模塊參與構(gòu)建;
  • 項(xiàng)目級(jí) build.gradle 文件: 用于定義所有子模塊公共的配置參數(shù)熄守;
  • 模塊級(jí) build.gradle 文件: 用于定義子模塊的配置參數(shù)蜈垮,它可以覆蓋項(xiàng)目級(jí) build.gradle 文件中定義的配置;
  • gradle/warpper: 負(fù)責(zé)自動(dòng)下載安裝項(xiàng)目所需的 Gradle 環(huán)境的腳本裕照;
  • gradle.properties: 用作項(xiàng)目級(jí) Gradle 配置項(xiàng)攒发,會(huì)覆蓋全局的配置項(xiàng);
  • local.properties: 用作項(xiàng)目的私有屬性配置晋南,例如 SDK 安裝目錄惠猿,一般不把 local.properties 加入版本控制。

1.3 Gradle 中的重要概念

  • Gradle: 提供核心構(gòu)建流程负间,但不提供具體構(gòu)建邏輯偶妖;
  • Gradle 插件: Gradle 提供的是一套核心的構(gòu)建機(jī)制,而 Gradle 插件正是運(yùn)行在這套機(jī)制上的一些具體構(gòu)建邏輯政溃,本質(zhì)上和 .gradle 文件沒有區(qū)別趾访。例如,我們熟悉的 Android 構(gòu)建流程就是由 Android Gradle Plugin 引入的構(gòu)建邏輯董虱;
  • Gradle Daemon: 用于提升構(gòu)建速度的后臺(tái)進(jìn)程扼鞋;
  • Gradle Wrapper: 對(duì) Gradle 的封裝,增加了自動(dòng)下載安裝 Gradle 環(huán)境的能力愤诱;
  • 環(huán)境變量 GRADLE: 用于定義 Gradle 的安裝目錄藏鹊;
  • 環(huán)境變量 GRADLE_USER_HOME: 用于定義 Gradle 運(yùn)行過程的文件存儲(chǔ)目錄,例如 Gradle Wrapper 自動(dòng)安裝的 Gradle 環(huán)境转锈、構(gòu)建緩存等盘寡;

1.4 Gradle Daemon

Gradle Daemon 是 Gradle 3.0 引入的構(gòu)建優(yōu)化策略,通過規(guī)避重復(fù)創(chuàng)建 JVM 和內(nèi)存緩存的手段提升了構(gòu)建速度撮慨。 Daemon 進(jìn)程才是執(zhí)行構(gòu)建的進(jìn)程竿痰,當(dāng)構(gòu)建結(jié)束后,Daemon 進(jìn)程并不會(huì)立即銷毀砌溺,而是保存在內(nèi)存中等待承接下一次構(gòu)建影涉。根據(jù)官方文檔說明,Gradle Daemon 能夠降低 15-75% 的構(gòu)建時(shí)間规伐。

Daemon 的優(yōu)化效果主要體現(xiàn)在 3 方面:

  • 1蟹倾、縮短 JVM 虛擬機(jī)啟動(dòng)時(shí)間: 不需要重復(fù)創(chuàng)建;
  • 2、JIT 編譯: Daemon 進(jìn)程會(huì)執(zhí)行 JIT 編譯鲜棠,有助于提升后續(xù)構(gòu)建的字節(jié)碼執(zhí)行效率肌厨;
  • 3、構(gòu)建緩存: 構(gòu)建過程中加載的類豁陆、資源或者 Task 的輸入和輸出會(huì)保存在內(nèi)存中柑爸,可以被后續(xù)構(gòu)建復(fù)用。

相關(guān)的 Gradle 命令:

  • gradle —status: 查看存活的 Daemon 進(jìn)程信息盒音;
  • gradle —stop: 停止所有 Daemon 進(jìn)程表鳍。

提示: 并不是所有的構(gòu)建都會(huì)復(fù)用同一個(gè) Daemon 進(jìn)程,如果已存活的 Daemon 進(jìn)程無法滿足新構(gòu)建的需求祥诽,則 Gradle 會(huì)新建一個(gè)新的 Daemon 進(jìn)程譬圣。影響因素:

  • Gradle 版本: 不同 Gradle 版本的構(gòu)建不會(huì)關(guān)聯(lián)到同一個(gè) Daemon 進(jìn)程;
  • Gradle 虛擬機(jī)參數(shù): 不滿足的虛擬機(jī)參數(shù)不會(huì)關(guān)聯(lián)到同一個(gè) Daemon 進(jìn)程雄坪。

1.5 Gradle Wrapper

Gradle Wrapper 本質(zhì)是對(duì) Gradle 的一層包裝胁镐,會(huì)在執(zhí)行 Gradle 構(gòu)建之前自動(dòng)下載安裝 Gradle 環(huán)境。 在開始執(zhí)行 Gradle 構(gòu)建時(shí)诸衔,如果當(dāng)前設(shè)備中還未安裝所需版本的 Gradle 環(huán)境盯漂,Gradle Wrapper 會(huì)先幫你下載安裝下來,將來其他需要這個(gè) Gradle 版本的工程也可以直接復(fù)用笨农。

Android Studio 默認(rèn)使用 Gradle Wrapper 執(zhí)行構(gòu)建就缆,你可以在設(shè)置中修改這一行為:

命令行也有區(qū)分:

  • gradle :使用系統(tǒng)環(huán)境變量定義的 Gradle 環(huán)境進(jìn)行構(gòu)建;
  • gradlew :使用 Gradle Wrapper 執(zhí)行構(gòu)建谒亦。

為什么 Gradle 官方從早期就專門推出一個(gè)自動(dòng)安裝環(huán)境工具呢竭宰,我認(rèn)為原因有 2 個(gè):

  • 確保 Gradle 版本正確性: 鑒于 Gradle 有較弱向后兼容性的特點(diǎn),Gradle Wrapper 能夠從項(xiàng)目工程級(jí)別固化項(xiàng)目所需要的 Gradle 版本份招,從而確保同一個(gè)工程移植到其他電腦后能夠正確地切揭、可重復(fù)地構(gòu)建;
  • 減少了手動(dòng)安裝 Gradle 環(huán)境的工作量: 單單從 Gradle 4 到 Gradle 7 就有大大小小十幾個(gè)版本锁摔,而且每個(gè)工程所需要的 Gradle 版本不盡相同廓旬,使用 Gradle Wrapper 能夠減少手動(dòng)安裝環(huán)境的工作量;

簡單說下 Gradle Wrapper 相關(guān)的文件谐腰,主要有 4 個(gè):

  • gradlew & gradlew.bat: 在 Linux 或 Mac 上可用的 Shell 腳本孕豹,以及在 Window 上可用的 Batch 腳本,用于以 Gradle Wrapper 的方式執(zhí)行構(gòu)建十气。也就是說励背,在命令行使用 gradlew 才是基于 Gradle Wrapper 執(zhí)行的,而使用 gradle 命令是直接基于系統(tǒng)安裝的 Gradle 環(huán)境執(zhí)行編譯砸西;
  • gradle-wrapper.jar: 負(fù)責(zé)下載安裝 Gradle 環(huán)境的腳本叶眉;
  • gradle-wrapper.properties: Gradle Wrapper 的配置文件址儒,主要作用是決定 Gradle 版本和安裝目錄:
    • distributionBase + distributionPath:指定 Gradle 環(huán)境安裝路徑;
    • zipStoreBase + zipStorePath:指定 Gradle 安裝包的存儲(chǔ)路徑衅疙;
    • distributionUrl:指定版本 Gradle 的下載地址莲趣,通過這個(gè)參數(shù)可以配置項(xiàng)目工程所需要的 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 的默認(rèn)值是 用戶目錄/.gradle炼蛤,可以通過系統(tǒng)環(huán)境變量 GRADLE_USER_HOME 修改妖爷。

1.6 gradle.properties 構(gòu)建環(huán)境配置

Gradle 是運(yùn)行在 Java 虛擬機(jī)的蝶涩,gradle.properties 文件可以配置 Gradle 構(gòu)建的運(yùn)行環(huán)境理朋,并且會(huì)覆蓋 Android Studio 設(shè)置中的全局配置,完整構(gòu)建環(huán)境配置見官方文檔:Build Enviroment绿聘。常用的配置項(xiàng)舉例:

# Gradle Daemon 開關(guān)嗽上,默認(rèn) ture
org.gradle.daemon=true  

# 虛擬機(jī)參數(shù)
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8

# 多模塊工程并行編譯多個(gè)模塊,會(huì)消耗更多內(nèi)存
org.gradle.parallel=true  

除了構(gòu)建環(huán)境配置熄攘,其他配置也可以用類似的鍵值對(duì)方式放在 gradle.properties 中兽愤,并直接在 .gradle 文件中引用。


2. Groovy 必知必會(huì)

Groovy 是從 Java 虛擬機(jī)衍生出來的語言挪圾,由于我們都具備一定的 Java 基礎(chǔ)浅萧,所以我們沒有必要完全從零開始學(xué)習(xí) Groovy。梳理 Groovy 與 Java 之間有差異的地方哲思,或許是更高效的學(xué)習(xí)方式:

2.1 一些小差異

  • 分號(hào): 語句允許不以分號(hào) ; 結(jié)尾洼畅;
  • public: 默認(rèn)的訪問修飾符為 public;
  • getter / setter: Groovy 會(huì)為每個(gè) field 創(chuàng)建對(duì)應(yīng)的 getter / setter 方法棚赔,在訪問 obj.field / obj.field=”” 時(shí)帝簇,實(shí)際上是在訪問 getField() 和 setField(””);
  • 支持靜態(tài)類型和動(dòng)態(tài)類型: Groovy 既支持 Java 的靜態(tài)類型靠益,也支持通過 def 關(guān)鍵字聲明動(dòng)態(tài)類型(靜態(tài)類型和動(dòng)態(tài)類型的關(guān)鍵區(qū)別在于 ”類型檢查是否傾向于在編譯時(shí)執(zhí)行“丧肴。例如 Java 是靜態(tài)類型語言,意味著類型檢查主要由編譯器在編譯時(shí)完成)胧后;
  • 字符串: Groovy 支持三種格式定義字符串 —— 單引號(hào)芋浮、雙引號(hào)和三引號(hào)
    • 單引號(hào):純粹的字符串,與 Java 的雙引號(hào)字符串類似壳快;
    • 雙引號(hào):支持在引號(hào)內(nèi)通過 $ 關(guān)鍵字直接引用變量值途样;
    • 三引號(hào):支持換行。

2.2 函數(shù)

  • 函數(shù)定義: Groovy 支持通過返回類型或 def 關(guān)鍵字定義函數(shù)濒憋。def 關(guān)鍵字定義的函數(shù)如果沒有 return 關(guān)鍵字返回值何暇,則默認(rèn)會(huì)返回 null。例如:
// 使用 def 關(guān)鍵字
def methodName() {
    // Method Code
}

String methodName() {
    // Method Code
}
  • 參數(shù)名: Groovy 支持不指定參數(shù)類型凛驮。例如:
// 省略參數(shù)類型
def methodName(param1, param2) {
    // Method Code
}

def methodName(String param1, String param2) {
    // Method Code
}
  • 默認(rèn)參數(shù): Groovy 支持指定函數(shù)參數(shù)默認(rèn)值裆站,默認(rèn)參數(shù)必須放在參數(shù)列表末尾。例如:
def methodName(param1, param2 = 1) {
    // Method Code
}
  • 返回值: 可以省略 return,默認(rèn)返回最后一行語句的值宏胯。例如:
def methodName() {
    return "返回值"
}
等價(jià)于
def methodName() {
    "返回值"
}
  • invokeMethod & methodMissing:
    • invokeMethod: 分派對(duì)象上所有方法調(diào)用羽嫡,包括已定義和未定義的方法,需要實(shí)現(xiàn) GroovyInterceptable 接口肩袍;
    • methodMissing: 分派對(duì)象上所有為定義方法的調(diào)用杭棵。
// 實(shí)現(xiàn) GroovyInterceptable 接口,才會(huì)把方法調(diào)用分派到 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 支持通過 [] 關(guān)鍵字定義 List 列表或 Map 集合:

  • 列表: 例如 def list = [1, 2, 3, 4]
  • 集合: 例如 def map = [’name’:’Tom’, ‘a(chǎn)ge’:18]魂爪,空集合 [:]
  • 范圍: 例如 def range = 1 .. 10
  • 遍歷:
// 列表
def list = [10, 11, 12]
list.each { value ->
}
list.eachWIthIndex { value, index ->
}

// 集合
def map = [’name’:’Tom’, ‘a(chǎn)ge’:18]
map.each { key, value ->
}
map.eachWithIndex { entry, index ->
}
map.eachWithIndex { key, value, index ->
}

2.4 閉包

Groovy 閉包是一個(gè)匿名代碼塊,可以作為值傳遞給變量或函數(shù)參數(shù)艰管,也可以接收參數(shù)和提供返回值滓侍,形式上與 Java / Kotlin 的 lambda 表達(dá)式類似。例如以下是有效的閉包:

{ 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 的實(shí)例牲芋,使得閉包可以像其他類型的值一樣復(fù)制給變量撩笆。例如:
Closure c = { 123 }

// 當(dāng)然也可以用 def 關(guān)鍵字
def c = { 123 }
  • 閉包調(diào)用: 閉包可以像方法一樣被調(diào)用,可以通過 Closure#call() 完成缸浦,也可以直接通過變量完成夕冲。例如:
def c = { 123 }

// 通過 Closure#call() 調(diào)用
c.call()

// 直接通過變量名調(diào)用
c()
  • 隱式參數(shù): 閉包默認(rèn)至少有一個(gè)形式參數(shù),如果閉包沒有顯式定義參數(shù)列表(使用 )裂逐,Groovy 總是帶有隱式添加一個(gè)參數(shù) it歹鱼。如果調(diào)用者沒有使用任何實(shí)參,則 it 為空絮姆。當(dāng)你需要聲明一個(gè)不接收任何參數(shù)的閉包醉冤,那么必須用顯式的空參數(shù)列表聲明。例如:
// 帶隱式參數(shù) it
def greeting = { "Hello, $it!" }
assert greeting('Patrick') == 'Hello, Patrick!'

// 不帶隱式參數(shù) it
def magicNumber = { -> 42 }
// error 不允許傳遞參數(shù)
magicNumber(11)
  • 閉包參數(shù)簡化: 函數(shù)的最后一個(gè)參數(shù)是閉包類型的化篙悯,在調(diào)用時(shí)可以簡化蚁阳,省略圓括號(hào):
def methodName(String param1, Closure closure) {
    // Method Code
}

// 調(diào)用:
methodName("Hello") {
    // Closure Code
}
  • this、owner鸽照、delegate: 閉包委托是 Groovy Closure 相比 Java Lambda 最大的區(qū)別螺捐,通過修改閉包的委托可以實(shí)現(xiàn)靈活多樣的 DSL。先認(rèn)識(shí)閉包中的三個(gè)變量:
    • this: 定義閉包的外部類矮燎,this 一定指向類對(duì)象定血;
    • owner: 定義閉包的外部對(duì)象,owner 可能是類對(duì)象诞外,也可能是更外一層的閉包澜沟;
    • delegate: 默認(rèn)情況 delegate 等同于 owner,this 和 owner 的語義無法修改峡谊,而 delegate 可以修改茫虽。
  • 閉包委托策略: 在閉包中刊苍,如果一個(gè)屬性沒有顯式聲明接收者對(duì)象,則會(huì)通過閉包代理解析策略尋找定義的對(duì)象濒析,例如:
class Person {
    String name
}
def p = new Person(name:'Igor')
def cl = { 
    // 相當(dāng)于 delegate.name.toUpperCase()
    name.toUpperCase() 
}                 
cl.delegate = p                                 
assert cl() == 'IGOR'

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

  • Closure.OWNER_FIRST(默認(rèn)): 優(yōu)先在 owner 對(duì)象中尋找,再去 delegate 對(duì)象中尋找号杏;
  • Closure.DELEGATE_FIRST: 優(yōu)先在 delegate 對(duì)象中尋找婴氮,再去 owner 對(duì)象中尋找;
  • Closure.OWNER_ONLY: 只在 owner 對(duì)象中尋找盾致;
  • Closure.DELEGATE_ONLY: 只在 delegate 對(duì)象中尋找主经;
  • Closure.TO_SELF: 只在閉包本身尋找;

3. Gradle 構(gòu)建生命周期

Gradle 將構(gòu)建劃分為三個(gè)階段: 初始化 - 配置 - 執(zhí)行 绰上。理解構(gòu)建生命周期(Gradle Build Lifecycle)非常重要旨怠,否則你可能連腳本中的每個(gè)代碼單元的執(zhí)行時(shí)機(jī)都搞不清楚渠驼。

3.1 初始化階段

由于 Gradle 支持單模塊構(gòu)建或多模塊構(gòu)建蜈块,因此在初始化階段(Initialization Phase),Gradle 需要知道哪些模塊將參與構(gòu)建迷扇。主要包含 4 步:

  • 1百揭、執(zhí)行 Init 腳本: Initialization Scripts 會(huì)在構(gòu)建最開始執(zhí)行,一般用于設(shè)置全局屬性蜓席、聲明周期監(jiān)聽器一、日志打印等。Gradle 支持多種配置 Init 腳本的方法厨内,以下方式配置的所有 Init 腳本都會(huì)被執(zhí)行:
    • gradle 命令行指定的文件:gradle —init-script <file>
    • USER_HOME/.gradle/init.gradle 文件
    • USER_HOME/.gradle/init.d/ 文件夾下的 .gradle 文件
    • GRADLE_HOME/init.d/ 文件夾下的 .gradle 文件
  • 2祈秕、實(shí)例化 Settings 接口實(shí)例: 解析根目錄下的 settings.gradle 文件,并實(shí)例化一個(gè) Settings 接口實(shí)例雏胃;
  • 3请毛、執(zhí)行 settings.gradle 腳本: 在 settings.gradle 文件中的代碼會(huì)在初始化階段執(zhí)行;
  • 4瞭亮、實(shí)例化 Project 接口實(shí)例: Gradle 會(huì)解析 include 聲明的模塊方仿,并為每個(gè)模塊 build.gradle 文件實(shí)例化 Project 接口實(shí)例。Gradle 默認(rèn)會(huì)在工程根目錄下尋找 include 包含的項(xiàng)目统翩,如果你想包含其他工程目錄下的項(xiàng)目仙蚜,可以這樣配置:
// 引用當(dāng)前工程目錄下的模塊
include ':app'

// 引用其他工程目錄下的模塊
include 'video' // 易錯(cuò)點(diǎn):不要加’冒號(hào) :‘
project(:video).projectDir = new File("..\\libs\\video")

提示: 模塊 build.gradle 文件的執(zhí)行順序和 include 順序沒有關(guān)系。

3.2 配置階段

配置階段(Configuration Phase)將執(zhí)行 build.gradle 中的構(gòu)建邏輯厂汗,以完成 Project 的配置委粉。主要包含 3 步:

  • 1、下載插件和依賴: Project 通常需要依賴其他插件或 Project 來完成工作娶桦,如果有需要先下載贾节;
  • 2匣掸、執(zhí)行腳本代碼: 在 build.gradle 文件中的代碼會(huì)在配置階段執(zhí)行;
  • 3氮双、構(gòu)造 Task DAG: 根據(jù) Task 的依賴關(guān)系構(gòu)造一個(gè)有向無環(huán)圖碰酝,以便在執(zhí)行階段按照依賴關(guān)系執(zhí)行 Task。

提示: 執(zhí)行任何 Gradle 構(gòu)建命令戴差,都會(huì)先執(zhí)行初始化階段和配置階段送爸。

3.3 執(zhí)行階段

在配置階段已經(jīng)構(gòu)造了 Task DAG,執(zhí)行階段(Execution Phase)就是按照依賴關(guān)系執(zhí)行 Task暖释。這里有兩個(gè)容易理解錯(cuò)誤的地方:

  • 1袭厂、Task 配置代碼在配置階段執(zhí)行,而 Task 動(dòng)作在執(zhí)行階段執(zhí)行球匕;
  • 2纹磺、即使執(zhí)行一個(gè) Task,整個(gè)工程的初始化階段和所有 Project 的配置階段也都會(huì)執(zhí)行亮曹,這是為了支持執(zhí)行過程中訪問構(gòu)建模型的任何部分橄杨。

原文: 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.

介紹完三個(gè)生命周期階段后,你可以通過以下 Demo 體會(huì)各個(gè)代碼單元所處的執(zhí)行階段:

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.'
    }
    // 易錯(cuò)點(diǎn):這里在配置階段執(zhí)行
    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 在執(zhí)行階段執(zhí)行有一個(gè)特例照卦,即通過 Project#defaultTasks 指定默認(rèn)任務(wù)式矫,會(huì)在配置階段會(huì)執(zhí)行,見 第 6.2 節(jié) 役耕,了解即可采转。

3.4 生命周期監(jiān)聽

Gradle 提供了一系列監(jiān)聽構(gòu)建生命周期流程的接口,大部分的節(jié)點(diǎn)都有直接的 Hook 點(diǎn)瞬痘,這里我總結(jié)一些常用的:

  • 1故慈、監(jiān)聽初始化階段

Gradle 接口提供了監(jiān)聽 Settings 初始化階段的方法:

settings.gradle

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

// 所有 Project 對(duì)象創(chuàng)建(注意:此時(shí) build.gradle 中的配置代碼還未執(zhí)行)
gradle.projectsLoaded {
    ...
}
  • 2、監(jiān)聽配置階段

Project 接口提供了監(jiān)聽當(dāng)前 Project 配置階段執(zhí)行的方法框全,其中 afterEvaluate 常用于在 Project 配置完成后繼續(xù)增加額外的配置察绷,例如 Hook 構(gòu)建過程中的 Task。

// 執(zhí)行 build.gradle 前
project.beforeEvaluate { 
    ...
}

// 執(zhí)行 build.gradle 后
project.afterEvaluate { 
    ...
}

除此之外竣况,Gradle 接口也提供了配置階段的監(jiān)聽:

// 執(zhí)行 build.gradle 前
gradle.beforeProject { project ->
    ...
}

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

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

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

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

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

// Task DAG 構(gòu)造完畢
gradle.taskGraph.whenReady {   
}

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

// 所有 Project 的 build.gradle 執(zhí)行完畢
gradle.projectsEvaluated {
    ...
}
  • 3、監(jiān)聽執(zhí)行階段

Gradle 接口提供了執(zhí)行階段的監(jiān)聽:

gradle.addListener(new TaskExecutionListener() {

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

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

gradle.addListener(new TaskActionListener() {

    // 開始執(zhí)行 Action 列表前丹泉,回調(diào)時(shí)機(jī)略晚于 TaskExecutionListener#beforeExecute
    @Override
    void beforeActions(Task task) {
        ...
    }

    // 執(zhí)行 Action 列表完畢情萤,回調(diào)時(shí)機(jī)略早于 TaskExecutionListener#afterExecute
    @Override
    void afterActions(Task task) {
        ...
    }
})

// 執(zhí)行 Task 前
gradle.taskGraph.beforeTask { Task task ->
}

// 執(zhí)行 Task 后
gradle.taskGraph.afterTask { Task task, TaskState state ->
    if (state.failure) {
        println "FAILED"
    }
    else {
        println "done"
    }
}
  • 4、監(jiān)聽 Task 創(chuàng)建

TaskContainer 接口提供了監(jiān)聽 Task 添加的方法摹恨,可以在 Task 添加到 Project 時(shí)收到回調(diào):

tasks.whenTaskAdded { task ->
}
  • 5筋岛、監(jiān)聽構(gòu)建結(jié)束

當(dāng)所有 Task 執(zhí)行完畢,意味著構(gòu)建結(jié)束:

gradle.buildFinished {
    ...
}

4. Project 核心 API

Project 可以理解為模塊的構(gòu)建管理器晒哄,在初始化階段睁宰,Gradle 會(huì)為每個(gè)模塊的 build.gradle 文件實(shí)例化一個(gè)接口對(duì)象肪获。在 .gradle 腳本中編寫的代碼,本質(zhì)上可以理解為是在一個(gè) Project 子類中編寫的柒傻。

4.1 Project API

Project 提供了一系列操作 Project 對(duì)象的 API:

  • getProject(): 返回當(dāng)前 Project孝赫;
  • getParent(): 返回父 Project,如果在工程 RootProject 中調(diào)用红符,則會(huì)返回 null青柄;
  • getRootProject(): 返回工程 RootProject;
  • getAllprojects(): 返回一個(gè) Project Set 集合预侯,包含當(dāng)前 Project 與所有子 Project致开;
  • getSubprojects(): 返回一個(gè) Project Set 集合,包含所有子 Project萎馅;
  • project(String): 返回指定 Project双戳,不存在時(shí)拋出 UnKnownProjectException;
  • findProject(String): 返回指定 Project糜芳,不存在時(shí)返回 null飒货;
  • allprojects(Closure): 為當(dāng)前 Project 以及所有子 Project 增加配置;
  • subprojects(Closure): 為所有子 Project 增加配置耍目。

4.2 Project 屬性 API

Project 提供了一系列操作屬性的 API膏斤,通過屬性 API 可以實(shí)現(xiàn)在 Project 之間共享配置參數(shù):

  • hasProperty(String): 判斷是否存在指定屬性名徐绑;
  • property(Stirng): 獲取屬性值邪驮,如果屬性不存在則拋出 MissingPropertyException;
  • findProperty(String): 獲取屬性值傲茄,如果屬性不存在則返回 null毅访;
  • setProperty(String, Object): 設(shè)置屬性值,如果屬性不存在則拋出 MissingPropertyException盘榨。

實(shí)際上喻粹,你不一定需要顯示調(diào)用這些 API,當(dāng)我們直接使用屬性名時(shí)草巡,Gradle 會(huì)幫我們隱式調(diào)用 property() 或 setProperty()守呜。例如:

build.gradle

name => 相當(dāng)于 project.getProperty("name")
project.name = "Peng" => 相當(dāng)于 project.setProperty("name", "Peng")

4.2.1 屬性匹配優(yōu)先級(jí)

Project 屬性的概念比我們理解的字段概念要復(fù)雜些,不僅僅是一個(gè)簡單的鍵值對(duì)山憨。Project 定義了 4 種命名空間(scopes)的屬性 —— 自有屬性查乒、Extension 屬性、ext 屬性郁竟、Task玛迄。 當(dāng)我們通過訪問屬性時(shí),會(huì)按照這個(gè)優(yōu)先級(jí)順序搜索棚亩。

getProperty() 的搜索過程:

  • 1蓖议、自有屬性: Project 對(duì)象自身持有的屬性虏杰,例如 rootProject 屬性;
  • 2勒虾、Extension 屬性纺阔;
  • 3、ext 屬性修然;
  • 4州弟、Task: 添加到 Project 上的 Task 也支持通過屬性 API 訪問;
  • 5低零、父 Project 的 ext 屬性: 會(huì)被子 Project 繼承婆翔,因此當(dāng) 1 ~ 5 未命中時(shí),會(huì)繼續(xù)從父 Project 搜索掏婶。需要注意: 從父 Project 繼承的屬性是只讀的啃奴;
  • 6、以上未命中雄妥,拋出 MissingPropertyException 或返回 null最蕾。

setProperty() 的搜索路徑(由于部分屬性是只讀的,搜索路徑較短):

  • 1老厌、自有屬性
  • 2瘟则、ext 額外屬性

提示: 其實(shí)還有 Convention 命名空間,不過已經(jīng)過時(shí)了枝秤,我們不考慮醋拧。

4.2.2 Extension 擴(kuò)展

Extension 擴(kuò)展是插件為外部構(gòu)建腳本提供的配置項(xiàng),用于支持外部自定義插件的工作方式淀弹,其實(shí)就是一個(gè)對(duì)外開放的 Java Bean 或 Groovy Bean丹壕。例如,我們熟悉的 android{} 就是 Android Gradle Plugin 提供的擴(kuò)展薇溃。

關(guān)于插件 Extension 擴(kuò)展的更多內(nèi)容菌赖,見下一篇文章。

4.2.3 ext 屬性

Gradle 為 Project 和 Task 提供了 ext 命名空間沐序,用于定義額外屬性琉用。如前所述,子 Project 會(huì)繼承 父 Project 定義的 ext 屬性策幼,但是只讀的邑时。我們經(jīng)常會(huì)在 Root Project 中定義 ext 屬性,而在子 Project 中可以直接復(fù)用屬性值垄惧,例如:

項(xiàng)目 build.gradle

ext {
    kotlin_version = '1.4.31'
}

模塊 build.gradle

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

4.3 Project 文件 API

4.3.1 文件路徑

  • getRootDir(): Project 的根目錄(不是工程根目錄)
  • getProjectDir(): 包含 build 文件夾的項(xiàng)目目錄
  • getBuildDir(): build 文件夾目錄

4.3.2 文件獲取

  • File file(Object path): 獲取單個(gè)文件,相對(duì)位置從當(dāng)前 Project 目錄開始
  • ConfigurableFileCollection files(Object... paths): 獲取多個(gè)文件到逊,相對(duì)位置從當(dāng)前 Project 目錄開始
def destFile = file('releases.xml')
if (destFile != null && !destFile.exists()) {
    destFile.createNewFile()
}

4.3.3 文件拷貝

  • copy(Closure): 文件拷貝铣口,參數(shù)閉包用于配置 CodeSpec 對(duì)象
copy {
    // 來源文件
    from file("build/outputs/apk")
    // 目標(biāo)文件
    into getRootProject().getBuildDir().path + "/apk/"
    exclude {
        // 排除不需要拷貝的文件
    }
    rename {
        // 對(duì)拷貝過來的文件進(jìn)行重命名
    }
}

4.3.4 文件遍歷

  • fileTree(Object baseDir): 將指定目錄轉(zhuǎn)化為文件樹滤钱,再進(jìn)行遍歷操作
fileTree("build/outputs/apk") { FileTree fileTree ->
    fileTree.visit { FileTreeElement fileTreeElement ->
        // 文件操作
    }
}

5. Task 核心 API

Project 的構(gòu)建邏輯由一系列 Task 的組成,每個(gè) Task 負(fù)責(zé)完成一個(gè)基本的工作脑题,例如 Javac 編譯 Task件缸、資源編譯 Task、Lint 檢查 Task叔遂,簽名 Task等他炊。在構(gòu)建配置階段,Gradle 會(huì)根據(jù) Task 的依賴關(guān)系構(gòu)造一個(gè)有向無環(huán)圖已艰,以便在執(zhí)行階段按照依賴關(guān)系執(zhí)行 Task痊末。

5.1 創(chuàng)建簡單 Task

Gradle 支持兩種創(chuàng)建簡單 Task 的語法:

  • 1、通過 task 關(guān)鍵字:
// 創(chuàng)建名為 MyTask 的任務(wù)
task MyTask(group: "MyGroup") {
    // Task 配置代碼
}
  • 2哩掺、通過 TaskContainer 方法: 通過 Project 的 TaskContainer 屬性凿叠,可以創(chuàng)建 Task,分為熱創(chuàng)建和懶創(chuàng)建:
    • Task create(String, Closure) 熱創(chuàng)建: 立即實(shí)例化 Task 對(duì)象嚼吞;
    • TaskProvider register(String, Closure) 懶創(chuàng)建: 注冊 Task 構(gòu)造器盒件,但不會(huì)實(shí)例化對(duì)象。創(chuàng)建 Task 操作會(huì)延遲到訪問該 Task 時(shí)舱禽,例如通過 TaskProvider#get() 或 TaskContainer#getByName()炒刁。
// 創(chuàng)建名為 MyTask 的任務(wù)
project.tasks.create(name: "MyTask") {
    // Task 配置代碼
}

5.2 創(chuàng)建增強(qiáng) Task(自定義 Task 類型)

除了簡單創(chuàng)建 Task 的方式,我們還可以自定義 Task 類型誊稚,Gradle 將這類 Task 稱為增強(qiáng) Task翔始。增強(qiáng) Task 的可重用性更好,并且可以通過暴露屬性的方式來定制 Task 的行為片吊。

  • 1绽昏、DefaultTask: 自定義 Task 必須繼承 DefaultTask。
class CustomTask extends DefaultTask {
    final String message
    final int number
}
  • 2俏脊、帶參數(shù)創(chuàng)建 Task: 除了可以在創(chuàng)建 Task 后配置屬性值,我們也可以在調(diào)用 TaskContainer#create() 時(shí)傳遞構(gòu)造器參數(shù)肤晓。為了將值傳遞給任務(wù)構(gòu)造函數(shù)爷贫,必須使用 @Inject 注解修飾構(gòu)造器。
class CustomTask extends DefaultTask {
    final String message
    final int number

    @Inject
    CustomTask(String message, int number) {
        this.message = message
        this.number = number
    }
}
// 第二個(gè)參數(shù)為 Task 類型
tasks.register('myTask', CustomTask, 'hello', 42)

5.3 獲取已創(chuàng)建 Task

可以獲取 TaskContainer 中已創(chuàng)建的任務(wù)补憾,對(duì)于通過 register 注冊的任務(wù)會(huì)在這個(gè)時(shí)機(jī)實(shí)例化漫萄。例如:

  • Task getByName(String): 獲取 Task,如果 Task 不存在則拋出 UnKnownDomainObjectException盈匾;
  • Task findByName(String): 獲取 Task腾务,如果 Task 不存在則返回 null。
// 獲取已創(chuàng)建的 Task
project.MyTask.name => 等同于 project.tasks.getByName("MyTask").name

5.4 設(shè)置 Task 屬性

設(shè)置 Task 屬性的語法主要有三種:

  • 1削饵、在創(chuàng)建 Task 時(shí)設(shè)置
task MyTask(group: "MyGroup")
  • 2岩瘦、通過 setter 方法設(shè)置
task MyTask {
    group = "MyGroup" => 等同于 setGroup("MyGroup")
}
  • 3未巫、通過 ext 額外屬性設(shè)置: Task 也支持與 Project 類似的額外屬性。例如:
task MyTask(group:"111") {
    ext.goods = 2
}

ext.goods = 1

println MyTask.good

輸出:2

Task 常用的自有屬性如下:

屬性 描述
name Task 標(biāo)識(shí)符启昧,在定義 Task 時(shí)指定
group Task 所屬的組
description Task 的描述信息
type Task類型叙凡,默認(rèn)為 DefaultTask
actions 動(dòng)作列表
dependsOn 依賴列表

注意事項(xiàng):

  • 嚴(yán)格避免使用帶空格的 Task name,否則在一些版本的 Android Studio 中會(huì)被截?cái)嗝苣瑢?dǎo)致不兼容握爷;
  • Android Studio 的 Gradle 面板會(huì)按照 group 屬性對(duì) Task 進(jìn)行分組顯示。其中严里, Tasks 組為 Root Project 中的 Task新啼,其他分組為各個(gè) Project 中的 Task,未指定 group 的 Task 會(huì)分配在 other 中刹碾。

5.5 執(zhí)行 Task

  • 1师抄、命令行: gradlew :[模塊名]:[任務(wù)名],例如:gradlew -q :app:dependencies
  • 2教硫、IDE 工具: 通過 IDE 提供的用戶界面工具執(zhí)行叨吮,例如 Gradle 面板或綠色三角形,支持普通執(zhí)行和調(diào)試執(zhí)行瞬矩;
  • 3茶鉴、默認(rèn)任務(wù): 通過 Project#defaultTasks 可以指定 Project 配置階段的默認(rèn)任務(wù),在配置階段會(huì)執(zhí)行(這說明 Task 是有可能在配置階段執(zhí)行的景用,了解即可涵叮,不用鉆牛角尖)。

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 動(dòng)作

每個(gè) Task 內(nèi)部都保持了一個(gè) Action 列表 actions伞插,執(zhí)行 Task 就是按順序執(zhí)行這個(gè)列表割粮,Action 是比 Task 更細(xì)的代碼單元。Task 支持添加多個(gè)動(dòng)作媚污,Task 提供了兩個(gè)方法來添加 Action:

  • doFirst(Closure): 在 Action 列表頭部添加一個(gè) Action舀瓢;
  • doLast(Closure): 在 Action 列表尾部添加一個(gè) Action。
task MyTask

MyTask.doFirst{
    println "Action doFirst 1"
}

MyTask.doFirst{
    println "Action doFirst 2"
}

MyTask.doLast{
    println "Action doLast 1"
}

執(zhí)行 MyTask 輸出:

Action doFirst 2
Action doFirst 1
Action doLast 1

對(duì)于自定義 Task耗美,還可以通過 @TaskAction 注解添加默認(rèn) Action京髓。例如:

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

5.7 跳過 Task 的執(zhí)行

并不是所有 Task 都會(huì)被執(zhí)行,Gradle 提供了多個(gè)方法來控制跳過 Task 的執(zhí)行:

  • 1商架、onlyIf{}: 閉包會(huì)在即將執(zhí)行 Task 之前執(zhí)行堰怨,閉包返回值決定了是否執(zhí)行 Task;
  • 2蛇摸、enabled 屬性: Task 的 enabled 屬性默認(rèn)為 true备图,設(shè)置為 false 表示無效任務(wù),不需要執(zhí)行。

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

  • 3揽涮、Task 異常: Task 提供了兩個(gè)異常抠藕,能夠當(dāng) Action 執(zhí)行過程中拋出以下異常,將跳過執(zhí)行并繼續(xù)后續(xù)的構(gòu)建過程:
    • StopActionException 中斷當(dāng)前 Action绞吁,并繼續(xù)當(dāng)前 Task 的下一個(gè) Action幢痘;
    • StopExecutionException 中斷當(dāng)前 Task,并繼續(xù) Task 依賴樹上的下一個(gè) Action家破。
  • 4颜说、timeouts 屬性: 當(dāng) Task 執(zhí)行時(shí)間到達(dá) timeouts 超時(shí)時(shí)間時(shí),執(zhí)行線程會(huì)收到一個(gè)中斷信號(hào)汰聋,可以借此許控制 Task 的執(zhí)行時(shí)間(前提是 Task 要響應(yīng)中斷信號(hào))门粪。

5.8 Task 依賴關(guān)系

通過建立 Task 的依賴關(guān)系可以構(gòu)建完成的 Task 有向無環(huán)圖:

  • dependsOn 強(qiáng)依賴: Task 通過 dependsOn 屬性建立強(qiáng)依賴關(guān)系,可以直接通過 dependsOn 屬性設(shè)置依賴列表烹困,也可以通過 dependsOn() 方法添加一個(gè)依賴玄妈;
  • 輸入輸出隱式依賴: 通過建立 Task 之間的輸入和輸出關(guān)系,也會(huì)隱式建立依賴關(guān)系髓梅。例如 Transform Task 之間就是通過輸入輸出建立的依賴關(guān)系拟蜻。

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

// 添加依賴
task3.dependsOn(task1, task2)

依賴關(guān)系:task3 依賴于 [task1, task2],在執(zhí)行 task3 前一定會(huì)執(zhí)行 task1 和 task2

在某些情況下枯饿,控制兩個(gè)任務(wù)的執(zhí)行順序非常有用酝锅,而不會(huì)在這些任務(wù)之間引入顯式依賴關(guān)系,可以理解為弱依賴奢方。 任務(wù)排序和任務(wù)依賴關(guān)系之間的主要區(qū)別在于搔扁,排序規(guī)則不影響將執(zhí)行哪些任務(wù),只影響任務(wù)的執(zhí)行順序蟋字。

  • mustRunAfter 強(qiáng)制順序: 指定強(qiáng)制要求的任務(wù)執(zhí)行順序济赎;
  • shouldRunAfter 非強(qiáng)制順序: 指定非強(qiáng)制的任務(wù)執(zhí)行順序沪曙,在兩種情況下會(huì)放棄此規(guī)則:1贿讹、該規(guī)則造成環(huán)形順序养篓;2、并行執(zhí)行并且任務(wù)的所有依賴項(xiàng)都已經(jīng)完成嫉入。
task3 mustRunAfter(task1, task2)
task3 shouldRunAfter(task1, task2)

依賴關(guān)系:無焰盗,在執(zhí)行 task3 前不一定會(huì)執(zhí)行 task1 和 task2
順序關(guān)系:[task1, task2] 優(yōu)先于 task3

5.9 Finalizer Task

給一個(gè) Task 添加 Finalizer 終結(jié)器任務(wù)后,無論 Task 執(zhí)行成功還是執(zhí)行失敗咒林,都會(huì)執(zhí)行終結(jié)器,這對(duì)于需要在 Task 執(zhí)行完畢后清理資源的情況非常有用爷光。

// taskY 是 taskX 的終結(jié)器
taskX finalizedBy taskY

6. 增量構(gòu)建

6.1 什么是增量構(gòu)建垫竞?

任何構(gòu)建工具都會(huì)盡量避免重復(fù)執(zhí)行相同工作,這一特性稱為 Incremental Build 增量構(gòu)建,這一特性能夠節(jié)省大量構(gòu)建時(shí)間欢瞪。例如編譯過源文件后就不應(yīng)該重復(fù)編譯活烙,除非發(fā)生了影響輸出的更改(例如修改或刪除源文件)。

Gradle 通過對(duì)比自從上一次構(gòu)建之后遣鼓,Task 的 inputsoutputs 是否變化啸盏,來決定是否跳過執(zhí)行。如果相同骑祟,則 Gralde 認(rèn)為 Task 是最新的回懦,從而會(huì)跳過執(zhí)行。在 Build Outputs 中看到 Task 名稱旁邊出現(xiàn) UP-TO-DATE 標(biāo)志次企,即說明該 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 的輸入輸出時(shí)缸棵,要遵循一個(gè)原則:如果 Task 的一個(gè)屬性會(huì)影響輸出舟茶,那么應(yīng)該將該屬性注冊為輸入,否則會(huì)影響 Task 執(zhí)行堵第;相反吧凉,如果 Task 的一個(gè)屬性不會(huì)影響輸出,那么不應(yīng)該將該屬性注冊為輸入踏志,否則 Task 會(huì)在不必要時(shí)執(zhí)行阀捅。

6.2 Task 輸入輸出

大多數(shù)情況下,Task 需要接收一些 input 輸入狰贯,并生成一些 output 輸出也搓。例如編譯任務(wù),輸入是源文件涵紊,而輸出是 Class 文件傍妒。Task 使用 TaskInputsTaskOutputs 管理輸入輸出:

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

對(duì)于 Task 的輸入輸出颤练,我們用面向?qū)ο蟮母拍钊ダ斫馐菦]問題的。如果我們把 Task 理解為一個(gè)函數(shù)驱负,則 Task 的輸入就是函數(shù)的參數(shù)嗦玖,而 Task 的輸出就是函數(shù)的返回值。在此理解的基礎(chǔ)上跃脊,再記住 2 個(gè)關(guān)鍵點(diǎn):

  • 1宇挫、隱式依賴: 如果一個(gè) Task 的輸入是另一個(gè) Task 的輸出,Gradle 會(huì)推斷出兩者之間的強(qiáng)依賴關(guān)系酪术;
  • 2器瘪、在配置階段聲明: 由于 Task 的輸入輸出會(huì)用于構(gòu)建依賴關(guān)系翠储,那么我們應(yīng)該確保在配置階段定義輸入輸出,而不是在執(zhí)行階段定義橡疼。

Task 支持三種形式的輸入:

  • 1援所、簡單值: 包括數(shù)值、字符串和任何實(shí)現(xiàn) Serializable 的類欣除;
  • 2住拭、文件: 包括單個(gè)文件或文件目錄;
  • 3历帚、嵌套對(duì)象: 不滿足以上兩種條件滔岳,但其字段聲明為輸入。
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 輸入輸出校驗(yàn)

通過注解方式注冊輸入輸出時(shí)抹缕,Gradle 會(huì)在配置階段會(huì)對(duì)屬性值進(jìn)行檢查澈蟆。如果屬性值不滿足條件,則 Gradle 會(huì)拋出 TaskValidationException 異常卓研。特殊情況時(shí)趴俘,如果允許輸入為 null 值,可以添加 @Optional 注解表示輸入可空奏赘。

  • @InputFile: 驗(yàn)證該屬性值不為 null寥闪,并且關(guān)聯(lián)一個(gè)文件(而不是文件夾),且該文件存在磨淌;
  • @InputDirectory: 驗(yàn)證該屬性值不為 null疲憋,并且關(guān)聯(lián)一個(gè)文件夾(而不是文件),且該文件夾存在梁只;
  • @OutputDirectory: 驗(yàn)證該屬性值不為 null缚柳,并且關(guān)聯(lián)一個(gè)文件夾(而不是文件),當(dāng)該文件夾不存在時(shí)會(huì)創(chuàng)建該文件夾搪锣。

7. 總結(jié)

到這里秋忙,Gradle 基礎(chǔ)的部分就講完了,下一篇文章我們來討論 Gradle 插件构舟。提個(gè)問題灰追,你知道 Gradle 插件和 .gradle 文件有區(qū)別嗎?關(guān)注我狗超,帶你了解更多弹澎。


參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末苦蒿,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子渗稍,更是在濱河造成了極大的恐慌刽肠,老刑警劉巖溃肪,帶你破解...
    沈念sama閱讀 211,817評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件免胃,死亡現(xiàn)場離奇詭異音五,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)羔沙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門躺涝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人扼雏,你說我怎么就攤上這事坚嗜。” “怎么了诗充?”我有些...
    開封第一講書人閱讀 157,354評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵苍蔬,是天一觀的道長。 經(jīng)常有香客問我蝴蜓,道長碟绑,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,498評(píng)論 1 284
  • 正文 為了忘掉前任茎匠,我火速辦了婚禮格仲,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘诵冒。我一直安慰自己凯肋,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評(píng)論 6 386
  • 文/花漫 我一把揭開白布汽馋。 她就那樣靜靜地躺著侮东,像睡著了一般。 火紅的嫁衣襯著肌膚如雪豹芯。 梳的紋絲不亂的頭發(fā)上悄雅,一...
    開封第一講書人閱讀 49,829評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音告组,去河邊找鬼煤伟。 笑死,一個(gè)胖子當(dāng)著我的面吹牛木缝,可吹牛的內(nèi)容都是我干的便锨。 我是一名探鬼主播,決...
    沈念sama閱讀 38,979評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼我碟,長吁一口氣:“原來是場噩夢啊……” “哼放案!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起矫俺,我...
    開封第一講書人閱讀 37,722評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤吱殉,失蹤者是張志新(化名)和其女友劉穎掸冤,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體友雳,經(jīng)...
    沈念sama閱讀 44,189評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡稿湿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了押赊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片饺藤。...
    茶點(diǎn)故事閱讀 38,654評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖流礁,靈堂內(nèi)的尸體忽然破棺而出涕俗,到底是詐尸還是另有隱情,我是刑警寧澤神帅,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布再姑,位于F島的核電站,受9級(jí)特大地震影響找御,放射性物質(zhì)發(fā)生泄漏元镀。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評(píng)論 3 313
  • 文/蒙蒙 一萎坷、第九天 我趴在偏房一處隱蔽的房頂上張望凹联。 院中可真熱鬧,春花似錦哆档、人聲如沸蔽挠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽澳淑。三九已至,卻和暖如春插佛,著一層夾襖步出監(jiān)牢的瞬間杠巡,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評(píng)論 1 266
  • 我被黑心中介騙來泰國打工雇寇, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留氢拥,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,382評(píng)論 2 360
  • 正文 我出身青樓锨侯,卻偏偏與公主長得像嫩海,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子囚痴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容