寫給 Android 開發者的 Gradle 系列(一)基本姿勢
如果你對本文感興趣,也許你對我的公眾號也會有興趣,可掃下方二維碼或搜尋公眾微訊號:mxszgg
本文基於 Android Gradle plugin 3.0.1
Gradle 介紹
筆者認為能夠戳進這篇文章的讀者十之八九也是知道 Gradle 可以用來幹什麼,所以沒必要介紹什麼了,畢竟說一堆術語搞得大家都不懂很難堪(手動滑稽)。簡單來說,對於大部分的 Android 開發者來說 Gradle 是一個強大的工具,它提供便捷的方式幫助開發者構建 app。如果想看一下比較豐富的介紹的話可以檢視如何通俗地理解 Gradle?
如何學習 Gradle
接下來筆者會出一系列關於 Gradle 文章,但是授人魚不如授人以漁 ——
-
Gradle 基於 groovy 語言,groovy 官方文件連結戳我。當然,對於類似筆者這種比較懶的程式設計師來說一般會選擇搜一些中文文章來看,如附錄中的Gradle從入門到實戰 - Groovy基礎。好訊息是 groovy 與 java 相同是基於 jvm,所以理解起來並不是那麼困難,且對於日常開發來說,真的不需要學習多少內容。
-
Gradle DSL 學習。新建一個 Android 專案,可以看到
project/build.gradle
檔案中的內容類似如下:buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.0.1' } } allprojects { repositories { jcenter() } } task clean(type: Delete) { delete rootProject.buildDir }
如果你想知道 buildscript
和 allprojects
的含義就應該戳開上面的連結了。例如文件中說到 allprojects
是用於配置當前 project 和所有子 project 的,該方法將會在這些 project 中執行給定的閉包,那麼上述程式碼閉包中的 repositories
的意義同樣是可以在文件中找到的。
-
Android Plugin DSL 學習。新建一個 Android 專案,可以看到
project/app/build.gradle
檔案中的內容類似如下:apply plugin: 'com.android.application' android { compileSdkVersion 27 defaultConfig { applicationId "com.joker.cliptest" minSdkVersion 21 // ... } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.android.support:appcompat-v7:27.1.1' // ... }
那麼像 buildTypes
的意義是什麼就可以在上述文件中找到了。
-
Gradle task 學習。在前文提到的
project/build.gradle
檔案中最後有一個task clean(type: Delete) { delete rootProject.buildDir }
其實就是一個 task,關於 task 的更多姿勢在後一節中闡述。
前面共有四點,而且看起來每一點都需要花一定的時間去學習,其實不然,等到問題遇到了再去查文件就好了,尤其是前面提到的兩點 DSL 學習,其實日常開發中就已經能夠理解其中的含義了,說白了它們其實就是一些閉包、一些固定格式,正是因為它們的格式是固定的,task 才能夠讀取到相應的資料完成相應的事情,所以各位讀者大不必害怕 Gradle 學習成本高。
Gradle task
日常開發中,上一節提到的四點中,Gradle task 與大部分的開發者開發是最為緊密的。日常開發中開發者難免會進行 build/clean project、build apk 等操作 ——
實際上這些按鈕的底層實現都是通過 Gradle task 來完成的,只不過 IDE 使用 GUI 降低開發者們的使用門檻。當然,如果各位讀者足夠細心觀察的話,是能夠看到點選相應按鈕後在 Build 輸出欄中會輸出相應的資訊 ——
其實說到這裡各位讀者理應對 task 有一個基本的概念,一個 task 就是一個函式,沒有什麼神祕的地方。想要知道當前 Android 專案中共有哪些 task,可以在專案目錄下輸入:
./gradlew tasks(Mac、Linux)
gradlew tasks(Windows,後文同)
將會輸出類似以下資訊:
這僅是部分 task,如果想要檢視全部的 task 可以新增 --all
引數:
./gradlew tasks --all
拉到最後的 Other task 部分:
可以看到所有的 task,可能有經驗的讀者更是感覺到親切,提取部分 task 說明下:
compileDebugJavaWithJavac
:編譯 java 檔案processDebugManifest
:生成最終 AndroidManifest 檔案compileDebugAidl
:編譯 AIDL 檔案packageDebug
:打包成 apk
而如果為 release 包開啟了混淆,可看到還有 transformClassesAndResourcesWithProguardForRelease
task,即為 release 包混淆。
這些 task 實際上貫穿了開發者們的日常開發流程,只是 IDE 在上層封裝了一層,開發者們點點按鈕就完成了這些操作。當然也有讀者會疑惑,這些 task 從哪裡來的?為什麼沒有在 build.gradle
中看到蛛絲馬跡?這些 task 都是 plugin 中的,在 build.gradle
頂部 apply 的 plugin 中的(com.android.application/com.android.library
)。在後續的文章中筆者將會介紹如何找到這些 task 的原始碼、如何閱讀它們、如何寫 task、如何寫 plugin 以及解析較為知名的 gradle 外掛原始碼,這些也正是 Android 開發者學習 gradle 的目的所在。
Gradle 構建週期
-
Initialization
:Gradle支援單個和多專案構建。在 Initialization 階段,Gradle 將會確定哪些專案將參與構建,併為每個專案建立一個 Project 物件例項。對於 Android 專案來說即為執行setting.gradle
檔案。那麼日常開發中setting.gradle
檔案是如何書寫的呢,假設當前應用中除 app 以外還有一個 a module 和 b module,那麼setting.gradle
檔案應類似如下:include ':app', ':a', ':b'
所以 Gradle 將會為它們三個分別建立一個 Project 物件例項。
-
Configuration
:在這一階段專案配置物件,所有專案的構建指令碼將會被「執行」,這樣才能夠知道各個 task 之間的依賴關係。需要說的一點是,這裡提到的「執行」可能會稍有一些歧義:task a { } task testBoth { // 依賴 a task 先執行 dependsOn("a") println '我會在 Configuration 和 Execution 階段都會執行' doFirst { println '我僅會在 testBoth 的 Execution 階段執行' } doLast { println '我僅會在 testBoth 的 Execution 階段執行' } }
寫在 task 閉包中的內容是會在
Configuration
中就執行的,例如上面的dependsOn("a")
和println
內容;而 doFirst {}/doLast {} 閉包中的內容是在Execution
階段才會執行到(doFirst {}/ doLast {}
實際上就是給當前 task 新增 Listener,這些 Listeners 只會在當前 taskExecution
階段才會執行)。建議各位讀者將這個 task 複製黏貼到 app 的
build.gradle
中,然後觀察點選 Android Studio 的 Sync 按鈕(含Configuration
階段)和在命令列輸入./gradlew app:testBoth
時命令列(testBoth
的Execution
階段)輸出的結果。便可理解上述閉包內容的執行過程。當然你也可以檢視 Settings file 中的案例來理解。對於 Android 專案來說即為執行各個 module 下的
build.gradle
檔案,這樣各個build.gradle
檔案中的 task 的依賴關係就被確認下來了(例如assembleDebug
task 的執行依賴於其他 tasks 先執行,而這個依賴關係的確定就是在Configuration
階段)。 -
Execution
:task 的執行階段。首先執行doFirst {}
閉包中的內容,最後執行doLast {}
閉包中的內容。
在命令列中執行某一個 task,是可以清晰地看見每一個執行流程:
hook Gradle 構建過程
在命令列輸入 ./gradlew xxx
並按下回車之後的執行過程中,就會執行上面的流程,再通過一張深入理解Android之Gradle中的圖加深印象——
然而在筆者目前為止碰到的 Gradle 開發中,很少提及到上述的 hook 系列。用的比較多的一個是前文提到的 task 中的 doFirst {}/ doLast {}
,再一個就是 Project 在 Configuration
階段結束的 hook,前文也提到**Configuration
階段將會執行每一個 Project 的 build.gradle
檔案**,那麼如何監聽這每一個 Project 的引入後的點呢?通過 Project.afterEvaluate ——
//afterEvaluate { Project project ->
// println 'hook afterEvaluate'
//}
task hook { Project project ->
afterEvaluate {
println 'hook afterEvaluate'
}
}
那麼為什麼要 hook Project 的 afterEvaluate
階段呢?因為在 afterEvaluate
階段的時候,當前 Project 內的 task 資訊才能被掌握,例如想在 assembleDebug
task 前輸出一段資訊,那麼在 app/build.gradle
中撰寫以下程式碼:
// task badHook {
// tasks.findByName("assembleDebug").doFirst {
// println 'hook afterEvaluate from BadHook'
// }
// }
task assDHook {
afterEvaluate {
tasks.findByName("assembleDebug").doFirst {
println 'hook afterEvaluate from assHook'
}
}
}
此時如果呼叫 assembleDebug
task 的話,首先是 Initialization
階段,再是 Configuration
階段,該階段中當 appProject 到達 afterEvaluate
階段的時候 appProject 中的 tasks 資訊將會全部被獲取,當然這其中也包括 assembleDebug
task,所以此時再給它新增一個 doFirst {}
閉包便可以達到目的。而像 badHook
閉包的內容將會在 Initialization
階段執行,此時 appProject 並沒有獲取全部的 task 資訊,將會導致壓根找不到 assembleDebug
的錯誤。
後文
本文中並沒有總結太多的基礎知識,基本都以外鏈的形勢寫在了文中,因為筆者認為現在已經有大量的文章寫的很不錯了,所以不想大費筆墨撰寫,筆者挑了一些自認為較重要的點為後文做鋪墊,另外,文中還有大量的外鏈是外鏈到官方文件的,所以建議各位讀者在遇見問題的時候多多查閱文件。各位讀者可以在下文的附錄中閱讀更為基礎的 Gradle 知識: