十分鐘搞定 Gradle

Snake_sss發表於2019-04-05

專案經驗,如需轉載,請註明作者:Yuloran (t.cn/EGU6c76)

前言

學習過程中,什麼階段最痛苦?大概是某個知識點的碎片資訊學習了很多卻仍然無法窺其門徑,也就是似懂非懂的時候。對於 Gradle,筆者之前就是這種狀態。在親手完成了一個需求後,發現 Gradle 也不過如此。

由於筆者做需求時採用的是倒扒皮的方式,即先 google 搜尋如何解決問題,再閱讀官方 User Guide,最後總結反思,所以用了半天的時間,還踩了一些坑。如果按照本文介紹,按部就班地學習,大概十分鐘就夠了。所謂一通則百通,窺其門徑後,若有其它需求,直接查閱 API 即可。

案例

筆者是做安卓整機開發的,目前接手了一個新專案,其 APP 分為兩個版本,一個是系統預置(private),一個供其它品牌手機安裝使用(public)。其中 public apk 需要打包到 private apk 的 assets 目錄下,以在 private apk 上實現掃碼安裝 public apk 的功能。兩個版本的程式碼目前是手動維護,很不方便。筆者便想通過建立自定義的 Task,讓 Gradle 來自動構建。

問題

  • 如何建立 private、public 兩個 build variants(構建變體)?
  • 如何配置 public 版本在 private 版本之前構建(因為 private 版本依賴 public 版本生成的 apk)?
  • public 版本構建完成後,如何自動複製其生成的 apk 到 private 版本的 assets 目錄下?

解決方案

  • 關於構建變體,其實就是一次編譯,輸出多個版本的 apk,具體內容請參考官方文件中文版《配置構建變體》
  • 兩個構建變體,說明對應兩個 assemble task,那麼只要獲獲取到這兩個 task 物件,然後設定其依賴關係即可
  • Gradle 檔案支援 groovy 編寫,groovy 又是基於 java 的,所以即使不熟悉 groovy 的語法,也可以用 java 寫出來。不過對於複製這種操作,Gradle 有現成的 API

如何編寫

方案很清晰:assemblePublicApp -> deleteOldPublicApp -> signNewPublicApp -> copyNewPublicApp -> assemblePrivateApp

但是程式碼怎麼寫呢?筆者一時間感到無從下手。比如如何獲取兩個構建變體對應的 assemble task?如何建立一個 copy task?又如何在執行 copy task 之前先執行 delete task(刪除 assets 目錄下的舊 apk) 以及 sign task(簽名 public apk)?

筆者一頓 google 搜尋之後解決了這些問題,不過也踩了一個坑,就是自定義 task 內的程式碼執行時機不對。比如 deleteOldPublicApk task 中的日誌,總是在執行 gradle assemble 命令之後立即輸出,而不是在 assemblePublicApp task 之後輸出:

File -> Demo/app/build.gradle

android {
    ...
}

task deleteOldPublicApk(type: Delete) {
    println("-----------> delete the old pubic apk begin") // 注意:這麼寫程式碼會在配置階段立即執行
    delete 'src/privateApp/assets/Public.apk' // delete 方法繼承自 Delete task,所以是一個 Action,在執行階段才會被執行
    println("-----------> delete the old pubic apk end") // 注意:這麼寫程式碼會在配置階段立即執行
}

task signNewPublicApp() {
    doFirst {
        println 'sign the new public app' // 寫在 doFirst 或者 doLast 中,才會在執行階段被執行,具體見下文
    }
}

task copyNewPublicApp() {
    doLast {
        println 'copy the new public app'
    }
}

afterEvaluate {
    def assemblePublic = tasks.getByName('assemblePublicAppRelease')
    deleteOldPublicApk.dependsOn(assemblePublic)

    copyNewPublicApp.dependsOn(deleteOldPublicApk, signNewPublicApp)

    def assemblePrivate = tasks.getByName('assemblePrivateApp')
    assemblePrivate.dependsOn(copyNewPublicApp)
}

dependencies {
    ...
}
複製程式碼

如上所示的 deleteOldPublicApk task,只要在 terminal 中 輸入 gradlew assemble 必然會首先列印:

-----------> delete the old pubic apk begin
-----------> delete the old pubic apk end
複製程式碼

相信很多不熟悉 Gradle 的人都會犯這樣的錯誤,stackoverflow 上有人也發出了同樣的疑問 Why is my Gradle task always running?

後來筆者閱讀了 Gradle 的官方文件 《Build Lifecycle》,恍然大悟,應該這麼寫:

task deleteOldPublicApk(type: Delete) {
    doFirst {
        println("-----------> delete the old pubic apk begin")
    }
    delete 'src/privateApp/assets/Public.apk'
    doLast {
        println("-----------> delete the old pubic apk old")
    }
}
複製程式碼

痛定思痛,筆者決定將 Gradle 的入門在此做一個總結。

入門

Gradle 的入門其實很簡單,不需要深入學習 Groovy(隨用隨查),也不用記 Gradle 的 API(隨用隨查)。只需要瞭解幾個核心概念(構建模型、構建的生命週期、Project、Task、TaskContainer),就能做到一通百通了。

構建模型的核心

十分鐘搞定 Gradle

左邊是構建模型的抽象,右邊是一個 java 工程的具體實現。Gradle 的核心就是左邊的抽象模型(有向無環圖),也就是說一個完整的構建過程,其實就是一系列 Task 的有序執行。

構建生命週期

注意,這一小節尤為重要,特別是配置階段與執行階段的區別,一定要分清楚。

三個構建階段

  1. Initialization:配置構建環境以及有哪些 Project 會參與構建(解析 settings.build)
  2. Configuration:生成參與構建的 Task 的有向無環圖以及執行屬於配置階段的程式碼(解析 build.gradle)
  3. Execution:按序執行所有 Task

示例

File-> settings.gradle

println 'This is executed during the initialization phase.' // settings.gradle 中的程式碼在初始化階段執行
複製程式碼

File->Demo/app/build.gradle

println 'This is executed during the configuration phase.' // 在配置階段執行

// 普通的自定義 Task
task testBoth {
	doFirst {
	  println 'This is executed first during the execution phase.' // doFirst 中的程式碼在執行階段執行
	}
	doLast {
	  println 'This is executed last during the execution phase.' // doLast 中的程式碼在執行階段執行
	}
	println 'This is executed during the configuration phase as well.' // 非 doFirst 或者 doLast 中的程式碼,在配置階段執行
}

// 繼承自 Copy 的 TasK
task copyPublicApk(type: Copy) {
    doFirst {
        println("-----------> copy the new pubic apk begin")
    }
    // from, into, rename 都繼承自 Copy,所以即使直接寫也是在執行階段執行
    from 'build/outputs/apk/app-publicApp-release.apk'
    into file('src/privateApp/assets')
    rename { String fileName ->
        fileName = "Public.apk"
    }
    doLast {
        println("-----------> copy the new pubic apk end")
    }
}
複製程式碼

Project

一個 build.gradle 對應一個 Project 物件,在 gradle 檔案中可通過 project 屬性訪問該物件。而 rootProject 屬性代表的是根 Project 物件,即專案根目錄下的 build.gradle 檔案。

Project 由一系列的 task 組成,你可以自定義 task,也可以繼承已有的 task:

十分鐘搞定 Gradle

Project 還有自己的屬性和方法:

十分鐘搞定 Gradle

Task types 以及 Project 的屬性和方法都可以在 Groovy DSL Reference 中查到。

Task

在 gradle 檔案中,我們一般使用 task 關鍵字來定義一個 task,通過 task 的名字就可以直接訪問該 task 物件:

File -> Demo/app/build.gradle

task customTask() {
    doLast {
        println 'hello, this is a custom task'    
    }
}
複製程式碼

如何查詢一個 task 呢?通過 TaskContainer 物件,在 gradle 檔案中通過 tasks 屬性來訪問該物件:

File -> Demo/app/build.gradle

afterEvaluate {
    def aTask = tasks.getByName('assembleDebug')
    println "aTask name is ${aTask.name}"
    aTask.dependsOn(customTask)
}
複製程式碼

如上所示,我們獲取到了 assembleDebug 這個 Task 的例項,並設定它依賴之前定義的 customTask,所以執行 assembleDebug 時就會先執行 customTask。

TaskContainer 還有很多查詢 task 的方法,具體可以查詢 Task Container

Gradle API 查閱指導

瞭解了構建模型及三大階段,接下來就是如何查閱 API 手冊了。因為 Android Studio 對 Gradle 檔案的編寫支援很不友好,筆者經常會出現程式碼沒有智慧提示、無法自動補全、無法程式碼跳轉等問題,而且語法高亮也是弱的可憐。所以,必須掌握手動查閱 Gradle API 的方法。

不過現在 Gradle 檔案也可以使用 kotlin 編寫,語法清晰,可讀性好,而且支援語法高亮、程式碼補全、程式碼跳轉等。感興趣的可以參考官方遷移教程《Migrating build logic from Groovy to Kotlin》

離線檢視

Gradle 網站現在也可以正常訪問了,不過 Android Studio 在下載 Gradle 外掛時,已經自動將使用者指南、DSL參考、API參考下載到本地了:

十分鐘搞定 Gradle

  • dsl:裡面的內容跟 javadoc 差不多,不過是經過分類的,互動體檢比 API 文件要好,主要關注核心型別裡的 Project、Task 和 TaskType,具體關注裡面的屬性和方法,以及繼承的屬性和方法,用到什麼就去查什麼
  • javadoc:java api 文件,可以檢視類的繼承以及實現情況,快速索引
  • userguide:使用者指南,比如 build lifecycle 的介紹,不過 html 內部的連結點選無法跳轉,還好目錄下有個帶書籤的 pdf 版

線上文件

離線文件不一定是最新的,有需要時可以檢視線上文件

示例

下面這段配置大家應該都見過,我們現在想搞清楚裡面的 main 是什麼意思:

    sourceSets {
        main {
            manifest.srcFile 'AndroidManifest.xml'
            ...
        }
    }
複製程式碼

直接到離線的 javadoc 中查詢 SourceSet:

十分鐘搞定 Gradle

顯然 main 是一個 SourceSet 物件,名字為:

十分鐘搞定 Gradle

而 sourceSets 則是一個 SourceContainer 物件,組織並管理一系列的 SourceSet 物件。

Groovy API 查閱指導

對於 Android 開發者來說,學習 Groovy 主要是為了閱讀別人寫的 build.gradle 檔案是什麼意思,因為 Groovy 是基於 java 的,所以其實完全可以使用 java 語法,只是不夠簡潔而已。

筆者認為 Groovy 語法最蛋疼的地方就是函式呼叫的圓括號可以省略,而屬性賦值的 = 也可以省略,這很容易導致屬性賦值與函式呼叫傻傻分不清楚,比如:

def aMethod(String x, String y) {
    println(x + y)
}

android {
    aMethod 'groovy', '函式呼叫的圓括號可以省略'
    ...
    println "project desp is: $description"
    // description 是 Project 物件的屬性之一,此處將其重新賦值,且省略了 '='
    description 'The Gradle Groovy DSL allows to omit the = assignment operator when assigning properties'
    println "project desp is: $description"
}

dependencies {
    ...
}
複製程式碼

在 terminal 中輸入 gradlew assemble 將會輸出

groovy函式呼叫的圓括號可以省略
project desp is: null
project desp is: The Gradle Groovy DSL allows to omit the = assignment operator when assigning properties
複製程式碼

你看這個 aMethod 呼叫,像不像屬性賦值?你看這個屬性賦值,像不像函式呼叫?

十分鐘搞定 Gradle

以下來自官方遷移至 Kotlin 編寫 Gradle 檔案的吐槽:

As a first migration step, it is recommended to prepare your Groovy build scripts by

  • unifying quotes using double quotes,
  • disambiguating function invocations and property assignments (using respectively parentheses and assignment operator).

The latter is a bit more involved as it may not be trivial to distinguish function invocations and property assignments in a Groovy script. A good strategy is to make all ambiguous statements property assignments first and then fix the build by turning the failing ones to function invocations.

建議按照以下章節順序,快速學習併入門 Groovy

結語

可以說 Groovy 所允許的各種省略是導致 Gradle 難以學習的罪魁禍首,雖然程式碼簡潔了,不過可讀性卻差了很多。不過 Groovy 中的很多語法還是很通用的,比如方法的具名引數、引數預設值以及字串內插等,這在 kotlin 中也有對應的語法,就是寫法有些許差異而已。

所謂難而不會,會而不難,希望看完本文,各位都能有一種 Gradle 也不過如此的感覺。

十分鐘搞定 Gradle

上文所述皆為 Gradle 公共 API,作為 Android 開發者還需瞭解 Android 專屬的 API:

Android Plugin DSL Reference

相關文章