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

原文

十分鐘搞定 Gradle - 掘金?

juejin.im

前言

學習過程中,什麼階段最痛苦?大概是某個知識點的碎片信息學習了很多卻仍然無法窺其門徑,也就是似懂非懂的時候。對於 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),就能做到一通百通了。

構建模型的核心

左邊是構建模型的抽象,右邊是一個 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:

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

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參考下載到本地了:

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

在線文檔

離線文檔不一定是最新的,有需要時可以查看在線文檔

  • dsl
  • javadoc
  • userguide

示例

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

sourceSets {
main {
manifest.srcFile AndroidManifest.xml
...
}
}

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

顯然 main 是一個 SourceSet 對象,名字為:

而 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 調用,像不像屬性賦值?你看這個屬性賦值,像不像函數調用?

以下來自官方遷移至 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:

  • Variable definition:了解變數是怎麼定義的,記住 def 這個關鍵字,可以用來定義變數、方法和閉包
  • Optionality:了解函數調用的圓括弧是怎麼省略的
  • Strings: 字元串的定義方式,以及如何在字元串中引用字元串變數(String interpolation)
  • Method definition、Named parameters、Default arguments:了解怎麼定義方法、方法的具名參數、方法參數的默認值
  • Fields and properties:了解欄位與屬性的區別
  • Closures:什麼是閉包,以及如何定義閉包(其實就是匿名函數)

結語

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

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

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

Android Plugin DSL Reference

推薦閱讀:

相关文章