Gradle 學習之 Task

本文內容大部分來自 Gradle 官方文檔,英文 OK 的同窗能夠直接看官方文檔。 本文示例代碼已放在 zjxstar 的 GitHubhtml

前言

上一篇文章中,咱們學習了 Gradle 中 Project 的相關知識,也提到了簡單 Task 的定義。一個 Project 裏包含了多個 Task(任務),Gradle 的一系列操做都歸功於 Task。本文將深刻學習 Task 的相關知識,掌握如何建立、訪問和配置一個 Task。java

什麼是 Task

Task(任務)表示構建過程當中的單個原子工做,例如編譯類或生成 javadoc。每一個任務都屬於某個 Project,在 Project 中能夠經過任務名或者 TaskContainer 來訪問任務。每一個任務都有一個徹底限定的路徑,該路徑在全部項目的全部任務中都是惟一的。路徑是所在項目路徑和任務名稱的串聯,使用 : 字符分隔。git

當 Gradle 執行一個任務時,它能夠在控制檯 UI 和 Tooling API 中使用不一樣的結果標記任務。這些標籤基於任務是否具備要執行的操做,是否應該執行這些操做,是否執行了這些操做以及這些操做是否進行了任何更改。github

主要有如下 5 種標記:(在運行 Task 時增長 -i 或者 --info 便可查看)api

  • 無標籤或者 EXECUTED:任務執行了它的動做。
    1. 任務有動做,Gradle 已經肯定它們應做爲構建的一部分執行。
    2. 任務沒有動做,但有一些依賴項,而且有依賴項獲得了執行。
  • UP-TO-DATE:任務的輸出沒有改變。
    1. 任務有輸出和輸入,但沒有改變。
    2. 任務有動做,但任務告訴 Gradle 它沒有改變它的輸出。
    3. 任務沒有動做,有依賴,但全部的依賴項都是 up-to-date 的、跳過的或者來自緩存。
    4. 任務沒有動做,也沒有依賴項。
  • FROM-CACHE:任務的輸出能夠從以前的執行中找到。任務具備從構建緩存恢復的輸出。
  • SKIPPED:任務沒有執行它的動做。
    1. 任務已經明確從命令行中排除。
    2. 任務有一個返回 false 的 onlyIf 斷言。
  • NO-SOURCE:任務不須要執行它的動做。任務有輸入和輸出,可是沒有來源。

建立 Task

在 Gradle 中,咱們有多種方式來建立任務。主要用到 Project 提供的 task 方法以及 TaskContainer ( tasks ) 提供的 create 方法。緩存

方式一:使用字符串做爲任務名建立 Task,示例:gradlew -q helloA閉包

task('helloA') {
    doLast {
        println("helloA task(name)")
    }
}
複製代碼

方式二:使用 tasks 的 create 方法,示例:gradlew -q helloBapp

tasks.create('helloB') {
    doFirst {
        println("helloB tasks.create(name)")
    }
}
複製代碼

方式三:使用 DSL 的特殊語法,示例:gradlew -q helloCide

task helloC {
    doLast {
        println("helloC task name")
    }
}

// 執行 gradlew -q helloD 命令運行
task(helloD) {
    doLast {
        println("helloD task(name)")
    }
}
複製代碼

在建立 Task 時,能夠傳入一個 Map 來簡單配置任務,經常使用配置項有:學習

配置項 描述 默認值
type 基於一個存在的 Task 來建立,相似於繼承 DefaultTask
overwrite 是否替換已經存在的 Task,和 type 配合使用 false
dependsOn 用於配置任務依賴 []
action 添加到任務中的一個 Action 或者一個閉包 null
description 用於配置任務的描述 null
group 用於配置任務的分組 null

使用示例:

// 使用 Map 增長配置項
task copy(type: Copy)

// 覆蓋了原 copy 任務
task copy(overwrite: true) {
    doLast {
        println('I am the new one.')
    }
}

// 使用 gradlew tasks 命令查看 helloE 的配置
task helloE {
    description 'i am helloE'
    group BasePlugin.BUILD_GROUP
    doLast {
        println('this is helloE')
    }
}
複製代碼

以上是建立一個 Task 的基本方法。但在 build.gradle 腳本中,咱們能夠利用 Groovy 語言的強大特性來動態建立多個任務。例如:gradlew -q task1

// 同時建立4個任務:task0、task一、task二、task3
4.times { counter ->
    task "task$counter" {
        doLast {
            println "I'm task number $counter"
        }
    }
}
複製代碼

訪問 Task

當建立完 Task 以後,咱們能夠訪問它們以進行配置或者依賴。那麼怎麼訪問一個已經定義好的 Task 呢?主要有三種方法。

方式一:使用 Groovy 中 DSL 特殊語法訪問。

// 承接上文示例
// 以helloE任務爲例
println '任務helloE的name: ' + helloE.name
println '任務helloE的description: ' + project.helloE.description
複製代碼

方式二:使用 tasks 訪問任務集。

// 利用tasks
println tasks.named('helloD').get().name
println tasks.copy.doLast {
    println 'configure by tasks.copy.doLast'
}
println tasks['helloC'].name
println tasks.getByName('helloB').name
複製代碼

方式三:經過路徑訪問任務

println tasks.getByPath('helloE').path // 找不到拋異常UnknownTaskException
println tasks.getByPath(':app:helloE').path
def ehelloE = tasks.findByPath("EhelloE") // 找不到返回null;
println ehelloE == null
複製代碼

經過路徑查找任務有兩種方法,一種是 get,另外一種是 find 。它們的區別在於 get 方法若是找不到指定任務就會拋出 UnknownTaskException 異常,而 find 方法則會返回 null 。

既然可以訪問到 Task,那就能夠對 Task 進行一些操做了,而這些操做又涉及到 Task 的屬性和方法,這裏簡單介紹下。

Task 的屬性範圍有四個。你能夠經過屬性名或者 Task.property( java.lang.String ) 方法訪問到指定屬性。也能夠經過 Task.setProperty( java.lang.String, java.lang.Object ) 方法修改屬性值。四個範圍以下:

  • Task 對象自己屬性。這包括 Task 實現類中聲明的任何帶有 getters 和 setters 方法的屬性。根據相應的 getter 和 setter 方法的存在,此範圍的屬性是可讀寫的。
  • 插件添加到任務的擴展。每一個擴展名均可以做爲只讀屬性使用,其名稱與擴展名相同。
  • 經過插件添加到任務的約定屬性。插件經過 Convention 對象向任務添加屬性和方法。此範圍的屬性可讀寫性取決於約定對象。
  • 額外屬性。每一個任務對象都維護了一個附加屬性的映射,是名稱 -> 值對,可用於動態地向任務對象添加屬性。此範圍的屬性是可讀寫的。

經常使用屬性有:

屬性名 描述
actions 該任務將要執行的一系列動做
dependsOn 返回該任務依賴的任務
description 任務的描述
enabled 該任務是否開啓
finalizedBy 返回完成此任務以後的任務
group 任務的分組
mustRunAfter 返回該任務必須在哪一個任務以後運行的任務
name 任務的名字
path 任務的路徑
project 任務所屬的 Project

咱們舉個額外屬性的例子:

// 定義 Task 的額外屬性
task myTask {
    ext.myProperty = "myValue"
}
// 訪問該屬性
task printTaskProperties {
    doLast {
        println myTask.myProperty
    }
}
複製代碼

其餘的屬性會在下面的小節中詳細介紹。

至於 Task 的方法,這裏只簡單列舉出來:

方法名(不列出參數) 描述
dependsOn 給任務設置依賴任務
doFirst 給 Task 添加一個任務動做開始執行以前的動做
doLast 給 Task 添加一個任務動做執行結束以後的動做
finalizedBy 給任務添加終結任務,即該任務結束後執行的任務
hasProperty 判斷該任務是否有指定屬性
mustRunAfter 聲明該任務必須在某些任務以後執行
onlyIf 給任務添加斷言,只有知足條件才能夠執行任務
property 返回指定屬性的值
setProperty 修改指定屬性的值

前面的例子中常常看見 doFirst 和 doLast,下文中會對它們作詳細的介紹。

DefaultTask

Gradle 其實還給咱們提供一個 DefaultTask 基類,經過繼承它能夠用來自定義任務,多用在自定義 Gradle 插件中。這裏簡單寫個示例,來講明 DefaultTask 的用法。

// app的build.gradle
class CustomTask extends DefaultTask {
    final String message
    final int number

    def content // 配置參數

    // 添加構造參數
    @Inject
    CustomTask(String message, int number) {
        this.message = message
        this.number = number
    }

    // 添加要執行動做
    @TaskAction
    def greet() {
        println content
        println "message is $message , number is $number !"
    }
}

// 使用tasks建立
// 須要傳遞兩個參數,不能爲null
tasks.create('myCustomTask1', CustomTask, 'hahaha', 110)
myCustomTask1.content = 'i love you'
myCustomTask1.doFirst {
    println 'my custom task do first'
}
myCustomTask1.doLast {
    println 'my custom task do last'
}

// 使用task建立,構造參數使用constructorArgs傳遞,參數不能爲null
task myCustomTask2(type: CustomTask, constructorArgs: ['xixi', 120])
myCustomTask2.content = 'i hate you'
複製代碼

示例中的 CustomTask 類是在 build.gradle 文件中定義的,直接繼承 DefaultTask 便可。若是但願該 Task 有可執行的動做,就須要在動做方法上添加 @TaskAction 的註解,這樣 Gradle 就會將該動做添加到 Task 的動做列表中。咱們能夠在類中爲任務配置屬性,如示例中的 content 、message 、number 。其中,message 和 number 經過 @javax.inject.Inject 註解設置成了構造參數,在建立該自定義任務時須要傳遞這兩個參數。

Task 的執行分析

講到這裏,咱們已經瞭解了 Task 的建立與使用,那麼如今有必要對 Task 的執行作一個大概的分析,這對咱們深刻理解 Task 有很大幫助。

當咱們執行一個 Task 的時候,實際上是執行其擁有的 actions 列表,這個列表保存在 Task 對象實例的 actions 成員變量中,其類型是一個 List:

private List<ContextAwareTaskAction> actions;
複製代碼

那麼怎麼將執行動做加入到該列表中呢?在前文的示例代碼中,你們應該注意到建立 Task 的時候用到了 doFirst 和 doLast 兩個方法。沒錯,這兩個方法就能夠將待執行的動做添加到 actions 列表中。

咱們粗略看下 doFirst 和 doLast 的源碼實現:(這兩個方法在類 org.gradle.api.internal.AbstractTask 中)

@Override
public Task doFirst(final String actionName, final Action<? super Task> action) {
    ...
    taskMutator.mutate("Task.doFirst(Action)", new Runnable() {
        public void run() {
            // 每次都添加到列表頭
            getTaskActions().add(0, wrap(action, actionName));
        }
    });
    return this;
}

@Override
public Task doLast(final String actionName, final Action<? super Task> action) {
    ...
    taskMutator.mutate("Task.doLast(Action)", new Runnable() {
        public void run() {
            // 每次都添加到表尾
            getTaskActions().add(wrap(action, actionName));
        }
    });
    return this;
}
複製代碼

能夠看到,doFirst 會把動做添加到表頭,而 doLast 則會把動做添加到表尾。

那怎麼把動做添加到列表中間呢?是否是直接在 Task 裏寫動做?咱們能夠試一下:

// 經過 gradlew -q greetC 執行
task greetC { // 能夠顯示聲明 doFirst doLast
    // 在配置階段執行
    println 'i am greet C, in configure'

    // 在執行階段執行
    doFirst {
        println 'i am greet C, in doFirst'
    }

    // 在執行階段執行
    doLast {
        println 'i am greet C, in doLast'
    }
}
複製代碼

執行該任務後,你會發現 " i am greet C, in configure " 這句話沒有在 doFirst 和 doLast 中間打印,而是打印在任務的配置階段。說明直接在 Task 裏寫的動做並不會添加到 Task 的動做列表中,只會當作 Task 的配置信息執行。那有沒有其餘辦法呢?

答案是確定的,這就得利用到上小節中提到的 DefaultTask 類了。咱們用 @TaskAction 註解標記的動做就會被添加到 Task 的動做列表中間。咱們直接執行上節中的 myCustomTask1 任務 ( gradlew -q myCustomTask1 ) 。結果以下:

my custom task do first // doFirst
i love you // @TaskAction
message is hahaha , number is 110 ! // @TaskAction
my custom task do last // doLast
複製代碼

這樣,Task 的執行順序基本就清晰了。

這裏須要提一個注意點,咱們在建立任務的時候,若是隻想給任務配置一個 doFirst 的動做,可使用左移符號 << 來表示。

task greetB << { // << 等價於 doFirst
    println 'i am greet B, in doFirst'

    // 不能繼續配置doLast
// doLast {
// println 'i am greet B, in doLast'
// }
}
// 只能單獨配置
greetB.doLast {
    println 'i am greet B, in doLast'
}
複製代碼

左移符號就等價於 doFirst ,並且,此時不能再給該任務配置 doLast 動做了,只能單獨進行配置。

Task 依賴和順序

咱們知道,一個 Project 擁有多個 Task,這些 Task 之間的關係由 DAG 圖維護。而 DAG 圖是在構建的配置過程當中生成的,咱們能夠經過 gradle.taskGraph 來監聽這個過程。

舉例:這個方法經常使用來給某些變量賦不一樣的值。( gradlew -q greetD )

def versionD = '0.0'
task greetD {
    doLast {
        println "i am greet D, versionD is $versionD"
    }
}
// task的DAG圖是在配置階段生成的
// 任務準備好了
gradle.taskGraph.whenReady {taskGraph ->
    if (taskGraph.hasTask('greetC')) {
        versionD = '1.0'
    } else {
        versionD = '1.0-alpha'
    }
}
// 任務執行前
gradle.taskGraph.beforeTask { Task task ->
    println "executing $task ..."
}
// 任務執行後
gradle.taskGraph.afterTask { Task task, TaskState state ->
    if (state.failure) {
        println "FAILED"
    } else {
        println "$task done"
    }
}
複製代碼

咱們能夠經過監聽 DAG 圖的回調來對特定的 Task 進行定製化處理。

既然任務是經過 DAG 圖維護的,那任務之間確定存在依賴和前後執行順序,咱們在定義任務的時候是否也能夠給任務添加依賴或者執行順序呢?這就得利用到任務的 dependsOn 、mustRunAfter 等方法了。

  • dependsOn :給某個任務設置依賴任務。

    task dependsA { // 定義一個基礎Task
        doLast {
            println 'i am depends A task'
        }
    }
    
    // 當執行B時,會先執行它的依賴任務A
    task dependsB {
        dependsOn dependsA // 經過方法設置
        doLast {
            println 'i am depends B task'
        }
    }
    
    // 經過Map參數依賴任務A
    task dependsC(dependsOn: dependsA) {
        doLast {
            println 'i am depends C task'
        }
    }
    
    // 任務D懶依賴任務E
    // 任務E後定義
    task dependsD {
        dependsOn 'dependsE'
        doLast {
            println 'i am depends D task'
        }
    }
    
    task dependsE {
        doLast {
            println 'i am depends E task'
        }
    }
    
    task dependsF {
        doLast {
            println 'i am depends F task'
        }
    }
    // 經過dependsOn方法同時依賴兩個任務E和A
    dependsF.dependsOn dependsE, dependsA
    複製代碼

    從示例中能夠看到,經過 dependsOn 設置任務的依賴關係後,當執行任務時,其依賴的任務會先完成執行。並且,能夠給某個任務同時設置多個依賴任務;也能夠進行懶依賴,即依賴那些尚未定義的任務。

  • finalizedBy :給某個任務設置終結任務。

    task taskX { // 定義任務X
        doLast {
            println 'i am task X'
        }
    }
    
    task taskY { // 定義任務Y
        doLast {
            println 'i am task Y'
        }
    }
    
    task taskZ { // 定義任務Z
        doLast {
            println 'i am task Z'
        }
    }
    // 任務X執行後,馬上執行任務Y和任務Z
    taskX.finalizedBy taskY, taskZ
    
    task taskM { // 定義任務M
        doLast {
            println 'i am task M'
        }
    }
    
    task taskN {
        finalizedBy taskM // 將任務M設置成任務N的終結任務
        doLast {
            println 'i am task N'
        }
    }
    複製代碼

    對任務進行 finalizedBy 配置和 dependsOn 很相似,其做用和 dependsOn 剛好相反。在某任務執行完後,會執行其設置的終結任務。

  • mustRunAfter :若是 taskB.mustRunAfter(taskA) 則表示 taskB 必須在 taskA 執行以後再執行,這個規則比較嚴格。

    task taskA {
        doLast {
            println 'i am task A'
        }
    }
    
    task taskB {
        doLast {
            println 'i am task B'
        }
    }
    // 任務A必須在任務B以後執行
    taskA.mustRunAfter taskB
    複製代碼

    運行命令 gradlew taskA taskB ,你會發現 taskB 會先執行。

  • shouldRunAfter :若是 taskB.shouldRunAfter(taskA) 則表示 taskB 應該在 taskA 以後執行,但這不是必須的,任務可能不會按預設的順序執行。

    task shouldTaskC << {
        println 'i am task C'
    }
    
    task shouldTaskD << {
        println 'i am task D'
    }
    
    shouldTaskC.shouldRunAfter shouldTaskD
    複製代碼

    運行命令 gradlew shouldTaskC shouldTaskD 查看執行結果。

    這裏舉個 shouldRunAfter 失效的例子:

    task shouldTaskX {
        doLast {
            println 'taskX'
        }
    }
    task shouldTaskY {
        doLast {
            println 'taskY'
        }
    }
    task shouldTaskZ {
        doLast {
            println 'taskZ'
        }
    }
    shouldTaskX.dependsOn shouldTaskY
    shouldTaskY.dependsOn shouldTaskZ
    shouldTaskZ.shouldRunAfter shouldTaskX // 這裏實際上是失效的
    複製代碼

    運行命令 gradlew -q shouldTaskX 會發現任務 Z 會先於任務 X 執行。

這裏再提一個 tasks 的 whenTaskAdded 方法。若是在構建過程當中有任務添加到 project ,則會觸發此回調。咱們能夠監聽這個回調來配置一些任務依賴或者修改某些變量,示例以下 ( gradlew HelloSecond )。

// 定義任務greetE
tasks.create('greetE') {
    doLast {
        println 'i am greetE'
    }
}
// 構建過程當中添加任務時會觸發此回調,經常使用來配置一些任務依賴或者賦值
// 經測試,該回調只針對插件中的任務有效
project.tasks.whenTaskAdded { task ->
    println "task ${task.name} add"
    if (task.name == 'HelloSecond') { // 執行HelloSecond任務時,會先執行greetE
        task.dependsOn 'greetE'
    }
}

// 定義一個自定義插件
class SecondPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        println 'Hello Second Gradle Plugin'
        // 添加一個task到project中
        project.task('HelloSecond', {
            println '===== SecondPlugin HelloSecond Task ====='
            logger.quiet('hello')
        })
    }
}

apply plugin: SecondPlugin
複製代碼

該回調只針對插件(插件的相關知識會在後續文章中給你們介紹)中定義的任務,示例中爲插件中的 HelloSecond 任務添加了一個 greetE 任務依賴。這樣,執行 HelloSecond 時會先執行 greetE 。

跳過 Task

可能你有這樣的需求:某些任務禁止執行或者知足某個條件才能執行。Gradle 提供了多種方式來跳過任務。

方式一:每一個任務都有個 enabled 屬性,能夠啓用和禁用任務,默認是 true,表示啓用。若是設置爲 false ,則會禁止該任務執行,輸出會提示該任務被跳過,即被標記成 SKIPPED 。

// 使用gradlew disableMe運行
// 輸出:Task :app:disableMe SKIPPED
task disableMe {
    doLast {
        println 'This should not be printed if the task is disabled.'
    }
}
disableMe.enabled = false // 禁止該任務執行
複製代碼

方式二:使用 onlyIf 判斷方法。只有當 onlyIf 裏返回 true 時該任務才能夠執行。

// 使用gradlew sayBye -PskipSayBye運行
// 這裏的-P是添加參數的意思
task sayBye {
    doLast {
        println 'i am sayBye task'
    }
}
// 只有當project中沒有 skipSayBye 屬性時,任務才能夠執行
sayBye.onlyIf { !project.hasProperty('skipSayBye') }
複製代碼

方式三:使用 StopExecutionException 。若是任務拋出這個異常,Gradle 會跳過該任務的執行,轉而去執行下一個任務。

// 使用gradlew nextTask運行
task byeTask {
    doLast {
        println 'We are doing the byeTask.'
    }
}
// 不會影響後續任務的執行
byeTask.doFirst {
    // Here you would put arbitrary conditions in real life.
    // But this is used in an integration test so we want defined behavior.
    if (true) { throw new StopExecutionException() }
}
task nextTask {
    dependsOn('byeTask') // 先執行byeTask,而byeTask會異常中斷
    doLast { // 並不影響nextTask的執行
        println 'I am not affected'
    }
}
複製代碼

方式四:利用 Task 的 timeout 屬性來限制任務的執行時間。一旦任務超時,它的執行就會被中斷,任務將被標記失敗。Gradle 中內置任務都能及時響應超時。

// 故意超時
task hangingTask() {
    doLast {
        Thread.sleep(100000)
    }
    timeout = Duration.ofMillis(500)
}
複製代碼

雖然有四種方法,但經常使用的仍是方法一和方法二。

Task 規則

若是咱們在要執行某個不存在的任務時,Gradle 會直接報異常提示找不到該任務。其實,咱們能夠經過添加規則的方式來作自定義處理。這須要利用到 TaskContainer 的 addRule 方法。

// 第一個參數是該規則的描述
// 第二個閉包參數是該規則要執行的動做
tasks.addRule("Pattern: ping<ID>") { String taskName ->
    if (taskName.startsWith("ping")) {
        task(taskName) {
            doLast {
                println "Pinging: " + (taskName - 'ping')
            }
        }
    }
}
複製代碼

例子中添加了一個規則:若是運行的任務是以 ping 開頭的,則會建立該任務(該任務運行前並不存在),並賦予 doLast 操做。可使用 gradlew pingServer1 執行。

咱們不只能夠經過命令行來使用規則,也能夠在基於規則的任務上建立 dependsOn 關係。

// gradlew groupPing
task groupPing {
    dependsOn pingServer1, pingServer2
}
複製代碼

生命週期任務

生命週期任務是不能自行完成的任務,它們一般沒有任何執行動做。這些任務主要體如今:

  • 工做流程任務,如: check ;
  • 一個可構建的東西,如: debug32MainExecutable ;
  • 用於執行多個有相同邏輯任務的便利任務,如: compileAll ;

除非生命週期任務具備操做,不然其結果由其依賴性決定。 若是執行任何任務的依賴項,則將認爲生命週期任務已執行。 若是全部任務的依賴項都是最新的,跳過的或來自緩存,則生命週期任務將被視爲最新的。

總結

讀完本文內容,相信你已經學會了如何建立和使用 Task 。Task 做爲 Gradle 的主要執行骨架是很是重要的,咱們能夠經過 Task 的各類屬性、方法來靈活地配置和調整任務的依賴、執行順序以及運行規則。你們不妨多寫一些示例,有助於理解 Gradle 的工做機制,也爲後續的自定義插件奠基基礎。

參考資料

  1. Gradle 官方文檔
  2. 《Android Gradle權威指南》
  3. 《Gradle for Android 中文版》
相關文章
相關標籤/搜索