十分鐘搞定 Gradle

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

前言

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

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

案例

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

問題

  • 如何建立 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 -> assemblePrivateAppgithub

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

筆者一頓 google 搜索以後解決了這些問題,不過也踩了一個坑,就是自定義 task 內的代碼執行時機不對。好比 deleteOldPublicApk task 中的日誌,老是在執行 gradle assemble 命令以後當即輸出,而不是在 assemblePublicApp task 以後輸出:api

File -> Demo/app/build.gradlebash

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?app

後來筆者閱讀了 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 版

在線文檔

離線文檔不必定是最新的,有須要時能夠查看在線文檔

示例

下面這段配置你們應該都見過,咱們如今想搞清楚裏面的 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

結語

能夠說 Groovy 所容許的各類省略是致使 Gradle 難以學習的罪魁禍首,雖然代碼簡潔了,不過可讀性卻差了不少。不過 Groovy 中的不少語法仍是很通用的,好比方法的具名參數、參數默認值以及字符串內插等,這在 kotlin 中也有對應的語法,就是寫法有些許差別而已。

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

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

Android Plugin DSL Reference

相關文章
相關標籤/搜索