請點贊加關注,你的支援對我非常重要,滿足下我的虛榮心。
? Hi,我是小彭。本文已收錄到 GitHub · Android-NoteBook 中。這裡有 Android 進階成長知識體系,有志同道合的朋友,歡迎跟著我一起成長。(聯絡方式在 GitHub)
前言
Gradle 本質上是高度模組化的構建邏輯,便於重用並與他人分享。例如,我們熟悉的 Android 構建流程就是由 Android Gradle Plugin 引入的構建邏輯。在這篇文章裡,我將帶你探討 Gradle 外掛的使用方法、開發步驟和技巧總結。
這篇文章是全面掌握 Gradle 構建系統系列的第 2 篇:
- 1、Gradle 基礎
- 2、Gradle 外掛
- 3、Gradle 依賴管理
- 4、APG Transform
1. 認識 Gradle 外掛
1.1 什麼是 Gradle 外掛
Gradle 和 Gradle 外掛是兩個完全不同的概念,Gradle 提供的是一套核心的構建機制,而 Gradle 外掛則是執行在這套機制上的一些具體構建邏輯,本質上和 .gradle 檔案是相同。例如,我們熟悉的編譯 Java 程式碼的能力,都是由外掛提供的。
1.2 Gradle 外掛的優點
雖然 Gradle 外掛與 .gradle 檔案本質上沒有區別,.gradle 檔案也能實現 Gradle 外掛類似的功能。但是,Gradle 外掛使用了獨立模組封裝構建邏輯,無論是從開發開始使用來看,Gradle 外掛的整體體驗都更友好。
- 邏輯複用: 將相同的邏輯提供給多個相似專案複用,減少重複維護類似邏輯開銷。當然 .gradle 檔案也能做到邏輯複用,但 Gradle 外掛的封裝性更好;
- 元件釋出: 可以將外掛釋出到 Maven 倉庫進行管理,其他專案可以使用外掛 ID 依賴。當然 .gradle 檔案也可以放到一個遠端路徑被其他專案引用;
- 構建配置: Gradle 外掛可以宣告外掛擴充套件來暴露可配置的屬性,提供定製化能力。當然 .gradle 檔案也可以做到,但實現會麻煩些。
1.3 外掛的兩種實現形式
Gradle 外掛的核心類是 PluginPlugin#apply()
方法,我們可以把 apply() 方法理解為外掛的執行入口。例如:
MyCustomGradlePlugin.groovy
public class MyCustomGradlePlugin implements Plugin<Project> {
@Override
void apply(Project project) {
println "Hello."
}
}
如果根據實現形式分類(MyCustomGradlePlugin 的程式碼位置),可以把 Gradle 外掛分為 2 類:
- 1、指令碼外掛: 指令碼外掛就是一個普通的指令碼檔案,它可以被匯入都其他構建指令碼中。有的朋友說指令碼外掛也需要使用 Plugin 介面才算指令碼外掛,例如:
build.gradle
apply plugin: MyCustomGradlePlugin
class MyCustomGradlePlugin implements Plugin<Project> {
...
}
- 2、二進位制外掛 / 物件外掛: 在一個單獨的外掛模組中定義,其他模組通過 Plugin ID 應用外掛。因為這種方式釋出和複用更加友好,我們一般接觸到的 Gradle 外掛都是指二進位制外掛的形式。
1.4 應用外掛的步驟
我們總結下使用二進位制外掛的步驟:
- 1、將外掛新增到 classpath: 將外掛新增到構建指令碼的 classpath 中,我們的 Gradle 構建指令碼才能應用外掛。這裡區分本地依賴和遠端依賴兩種情況。
本地依賴: 指直接依賴本地外掛原始碼,一般在除錯外掛的階段是使用本地依賴的方式。例如:
專案 build.gradle
buildscript {
...
dependencies {
// For Debug
classpath project(":easyupload")
}
}
遠端依賴: 指依賴已釋出到 Maven 倉庫的外掛,一般我們都是用這種方式依賴官方或第三方實現的 Gradle 外掛。例如:
專案 build.gradle
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.3'
// 也可以使用另一種等價語法:
classpath group: 'com.android.tools.build ', name: 'gradle ', version: '3.5.3'
}
...
}
- 2、使用 apply 應用外掛: 在需要使用外掛的 .gradle 指令碼中使用
apply
應用外掛,這將建立一個新的 Plugin 例項,並執行 Plugin#apply() 方法。例如:
apply plugin: 'com.android.application'
// 或者
plugins {
// id «plugin id» [version «plugin version»] [apply «false»]
id 'com.android.application'
}
注意: 不支援在一個 build.gradle 中同時使用這兩種語法。
1.5 特殊的 buildSrc 模組
外掛模組的名稱是任意的,除非使用了一個特殊的名稱 “buildSrc”,buildSrc 模組是 Gradle 預設的外掛模組。buildSrc 模組本質上和普通的外掛模組是一樣的,有一些小區別:
- 1、buildSrc 模組會被自動識別為參與構建的模組,因此不需要在 settings.gradle 中使用 include 引入,就算引入了也會編譯出錯:
Build OutPut:
'buildSrc' cannot be used as a project name as it is a reserved name
- 2、buildSrc 模組會自動被新增到構建指令碼的 classpath 中,不需要手動新增:
buildscript {
...
dependencies {
// 不需要手動新增
// classpath project(":buildSrc")
}
}
- 3、buildSrc 模組的 build.gradle 執行時機早於其他 Project:
Executing tasks: [test]
settings.gradle:This is executed during the initialization phase.
> Configure project :buildSrc
build.gradle:buildSrc.
> Task :buildSrc:compileJava NO-SOURCE
> Task :buildSrc:compileGroovy NO-SOURCE
> Task :buildSrc:pluginDescriptors UP-TO-DATE
> Task :buildSrc:processResources NO-SOURCE
> Task :buildSrc:classes UP-TO-DATE
> Task :buildSrc:jar UP-TO-DATE
> Task :buildSrc:assemble UP-TO-DATE
> Task :buildSrc:pluginUnderTestMetadata UP-TO-DATE
> Task :buildSrc:compileTestJava NO-SOURCE
> Task :buildSrc:compileTestGroovy NO-SOURCE
> Task :buildSrc:processTestResources NO-SOURCE
> Task :buildSrc:testClasses UP-TO-DATE
> Task :buildSrc:test NO-SOURCE
> Task :buildSrc:validatePlugins UP-TO-DATE
> Task :buildSrc:check UP-TO-DATE
> Task :buildSrc:build UP-TO-DATE
...
> Configure project :
...
> Task :test
...
BUILD SUCCESSFUL in 19s
2. 自定義 Gradle 外掛的步驟
這一節我們來講實現 Gradle 外掛的具體步驟,基本步驟分為 5 步:
- 1、初始化外掛目錄結構
- 2、建立外掛實現類
- 3、配置外掛實現類
- 4、釋出外掛
- 5、使用外掛
2.1 初始化外掛目錄結構
首先,我們在 Android Studio 新建一個 Java or Kotlin Library
模組,這裡以非 buildSrc 模組的情況為例:
然後,將模組 build.gradle 檔案替換為以下內容:
模組 build.gradle
plugins {
id 'groovy' // Groovy Language
id 'org.jetbrains.kotlin.jvm' // Kotlin
id 'java-gradle-plugin' // Java Gradle Plugin
}
- groovy 外掛: 使用 Groovy 語言開發必備;
- org.jetbrains.kotlin.jvm 外掛: 使用 Kotlin 語言開發必備;
- java-gradle-plugin 外掛: 用於幫助開發 Gradle 外掛,會自動應用 Java Library 外掛,並在 dependencies 中新增
implementation gradleApi()
。
最後,根據你需要的開發語言補充對應的原始碼資料夾,不同語言有預設的原始碼資料夾,你也可以在 build.gradle 檔案中重新指定:
模組 build.gradle
plugins {
id 'groovy' // Groovy Language
id 'org.jetbrains.kotlin.jvm' // Kotlin
id 'java-gradle-plugin' // Java Gradle Plugin
}
sourceSets {
main {
groovy {
srcDir 'src/main/groovy'
}
java {
srcDir 'src/main/java'
}
resources {
srcDir 'src/main/resources'
}
}
}
外掛目錄結構:
2.2 建立外掛實現類
新建一個 Plugin
com.pengxr.easyupload.EasyUpload.groovy
class EasyUpload implements Plugin<Project> {
@Override
void apply(Project project) {
// 構建邏輯
println "Hello."
}
}
2.3 配置外掛實現類
在模組 build.gradle 檔案中增加以下配置,gradlePlugin 定義了外掛 ID 和外掛實現類的對映關係:
gradlePlugin {
plugins {
modularPlugin {
// Plugin id.
id = 'com.pengxr.easyupload'
// Plugin implementation.
implementationClass = 'com.pengxr.easyupload.EasyUpload'
}
}
}
這其實是 Java Gradle Plugin 提供的一個簡化 API,其背後會自動幫我們建立一個 [外掛ID].properties 配置檔案,Gradle 就是通過這個檔案類進行匹配的。如果你不使用 gradlePlugin API,直接手動建立 [外掛ID].properties 檔案,作用是完全一樣的。
要點:
- 1、[外掛ID].properties 檔名是外掛 ID,用於應用外掛
- 2、[外掛ID].properties 檔案內容配置了外掛實現類的對映,需要使用
implementation-class
來指定外掛實習類的全限定類名
implementation-class=com.pengxr.easyupload.EasyUpload
2.4 釋出外掛
我們使用 maven 外掛 來發布倉庫,在模組 build.gradle 檔案中增加配置:
模組 build.gradle
plugins {
id 'groovy' // Groovy Language
id 'org.jetbrains.kotlin.jvm' // Kotlin
id 'java-gradle-plugin' // Java Gradle Plugin
}
gradlePlugin {
plugins {
modularPlugin {
// Plugin id.
id = 'com.pengxr.easyupload'
// Plugin implementation.
implementationClass = 'com.pengxr.easyupload.EasyUpload'
}
}
}
uploadArchives {
repositories {
mavenDeployer {
repository(url: uri('../localMavenRepository/snapshot'))
pom.groupId = 'com.pengxr'
pom.artifactId = 'easyupload'
pom.version = '1.0.0'
}
}
}
執行 uploadArchives
任務,會發布外掛到專案根目錄中的 localMavenRepository 資料夾,實際專案中通常是釋出到 Nexus 私庫或 Github 公共庫等。不熟悉元件釋出的話可以回顧:Android工程化實踐:元件化釋出,此處不展開。
2.5 使用外掛
在專案級 build.gradle 檔案中將外掛新增到 classpath:
專案 build.gradle
buildscript {
repositories {
google()
jcenter()
maven { url "$rootDir/localMavenRepository/snapshot" }
maven { url "$rootDir/localMavenRepository/release" }
}
dependencies {
// For debug
// classpath project(":easyupload")
classpath "com.pengxr:easyupload:1.0.0"
}
...
}
在模組級 build.gradle 檔案中 apply 外掛:
模組 build.gradle
// '專案 build.gradle' 是在 gradlePlugin 中定義的外掛 ID
apply plugin: 'com.pengxr.easyupload'
完成以上步驟並同步專案,從 Build Output 可以看到我們的外掛生效了:
Build Output:
Hello.
到這裡,自定義 Gradle 外掛最基本的步驟就完成了,接下來就可以在 Plugin#apply 方法中開始你的表演。
3. 外掛擴充套件機制
Extension 擴充套件是外掛為外部構建指令碼提供的配置項,用於支援外部自定義外掛的工作方式,其實就是一個對外開放的 Java Bean 或 Groovy Bean。例如,我們熟悉的 android{}
就是 Android Gradle Plugin 提供的擴充套件。
當你應用一個外掛時,外掛定義的擴充套件會以 副檔名-擴充套件物件
鍵值對的形式儲存在 Project 中的 ExtensionContainer 容器中。外掛內外部也是通過 ExtensionContainer 訪問擴充套件物件的。
注意事項:
- 副檔名: 不支援在同一個 Project 上新增重複的副檔名;
- 對映關係: 新增擴充套件後,不支援重新設定擴充套件物件;
- DSL: 支援用
副檔名 {}
DSL 的形式訪問擴充套件物件。
3.1 基本步驟
這一節我們來講實現 Extension 擴充套件的具體步驟,基本步驟分為 5 步:
- 1、定義擴充套件類: 定義一個擴充套件配置類:
Upload.groovy
class Upload {
String name
}
提示: 根據 ”約定優先於配置“ 原則,儘量為配置提供預設值,或者保證配置預設時也能正常執行。
- 2、建立並新增擴充套件物件: 在 Plugin#apply() 中,將擴充套件物件新增到 Project 的 ExtensionContainer 容器中:
EasyUpload.groovy
class EasyUpload implements Plugin<Project> {
// 副檔名
public static final String UPLOAD_EXTENSION_NAME = "upload"
@Override
void apply(Project project) {
// 新增擴充套件
applyExtension(project)
// 新增 Maven 釋出能力
applyMavenFeature(project)
}
private void applyExtension(Project project) {
// 建立擴充套件,並新增到 ExtensionContainer
project.extensions.create(UPLOAD_EXTENSION_NAME, Upload)
}
private void applyMavenFeature(Project project) {
// 構建邏輯
}
}
- 3、配置擴充套件: 使用方應用外掛後,使用
副檔名 {}
DSL定製外掛行為:
build.gradle
apply plugin: 'com.pengxr.easyupload'
upload {
name = "Peng"
}
- 4、使用擴充套件: 在 Plugin#apply() 中,通過 Project 的 ExtensionContainer 容器獲取擴充套件物件,獲取的程式碼建議封裝在擴充套件物件內部。例如:
class EasyUpload implements Plugin<Project> {
// 副檔名
public static final String UPLOAD_EXTENSION_NAME = "upload"
@Override
void apply(Project project) {
// 新增擴充套件
applyExtension(project)
// 新增 Maven 釋出能力
applyMavenFeature(project)
}
private void applyExtension(Project project) {
// 建立擴充套件,並新增到 ExtensionContainer 容器
project.extensions.create(UPLOAD_EXTENSION_NAME, Upload)
}
private void applyMavenFeature(Project project) {
project.afterEvaluate {
// 1. Upload extension
Upload rootConfig = Upload.getConfig(project.rootProject)
// 構建邏輯 ...
}
}
}
Upload.groovy
class Upload {
String name
// 將獲取擴充套件物件的程式碼封裝為靜態方法
static Upload getConfig(Project project) {
// 從 ExtensionContainer 容器獲取擴充套件物件
Upload extension = project.getExtensions().findByType(Upload.class)
// 配置預設的時候,賦予預設值
if (null == extension) {
extension = new Upload()
}
return extension
}
/**
* 檢查擴充套件配置是否有效
*
* @return true:valid
*/
boolean checkParams() {
return true
}
}
提示: ExtensionContainer#create() 支援變長引數,支援呼叫擴充套件類帶引數的建構函式,例如:
project.extensions.create(UPLOAD_EXTENSION_NAME, Upload,"Name")
將呼叫建構函式Upload(String str)
。
- 5、構建邏輯: 到這裡,實現外掛擴充套件最基本的步驟就完成了,接下來就可以在 Plugin#apply 方法中繼續完成你的表演。
3.2 project.afterEvaluate 的作用
使用外掛擴充套件一定會用到 project.afterEvaluate()
生命週期監聽,這裡解釋一下:因為擴充套件配置程式碼的執行時機晚於 Plugin#apply() 的執行時機,所以如果不使用 project.afterEvaluate(),則在外掛內部將無法正確獲取配置值。
project.afterEvaluate() 會在當前 Project 配置完成後回撥,這個時機擴充套件配置程式碼已經執行,在外掛內部就可以正確獲取配置值。
apply plugin: 'com.pengxr.easyupload'
// 執行時機晚於 apply
upload {
name = "Peng"
}
3.3 巢狀擴充套件
在擴充套件類中組合另一個配置類的情況,我們稱為巢狀擴充套件,例如我們熟悉的 defaultConfig{}
就是一個巢狀擴充套件:
android {
compileSdkVersion 30
buildToolsVersion "30.0.0"
defaultConfig {
minSdkVersion 21
...
}
...
}
預設下巢狀擴充套件是不支援使用閉包配置,我們需要在外部擴充套件類中定義閉包函式。例如:
Upload.groovy
class Upload {
// 巢狀擴充套件
Maven maven
// 巢狀擴充套件
Pom pom
// 巢狀擴充套件閉包函式,方法名為 maven(方法名不一定需要與屬性名一致)
void maven(Action<Maven> action) {
action.execute(maven)
}
// 巢狀擴充套件閉包函式,方法名為 maven
void maven(Closure closure) {
ConfigureUtil.configure(closure, maven)
}
// 巢狀擴充套件閉包函式,方法名為 pom
void pom(Action<Pom> action) {
action.execute(pom)
}
// 巢狀擴充套件閉包函式,方法名為 pom
void pom(Closure closure) {
ConfigureUtil.configure(closure, pom)
}
}
使用時:
build.gradle
apply plugin: 'com.pengxr.easyupload'
upload {
maven {
...
}
}
3.4 NamedDomainObjectContainer 命名 DSL
在 Android 工程中,你一定在 build.gradle 檔案中見過以下配置:
build.gradle
android {
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
debug {
...
}
// 支援任意命名
preview {
...
}
}
}
除了內建的 release 和 debug,我們可以在 buildType 中定義任意多個且任意名稱的型別,這個是如果實現的呢?—— 這背後是因為 buildTypes 是 NamedDomainObjectContainer 型別,原始碼體現:
com.android.build.api.dsl.CommonExtension.kt
val buildTypes: NamedDomainObjectContainer<BuildType>
NamedDomainObjectContainer 的作用:
NamedDomainObjectContainer
- Set 容器: 支援新增多個 T 型別物件,並且不允許命名重複;
- 命名 DSL: 支援以 DSL 的方式配置 T 型別物件,這也要求 T 型別必須帶有 String name 屬性,且必須帶有以 String name 為引數的 public 建構函式;
- SortSet 容器: 容器將保證元素以 name 自然順序排序。
那麼,以上配置相當於以下虛擬碼:
build.gradle
val buildTypes : Collections<BuildType>
BuildType release = BuildType("release")
release.minifyEnabled = false
release.proguardFiles = getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
BuildType debug = BuildType("debug")
...
BuildType preview = BuildType("preview")
...
buildTypes.add(release)
buildTypes.add(debug)
buildType.add(preview)
NamedDomainObjectContainer 的用法:
這裡介紹一下具體用法,我們僅以你熟悉的 BuildType 為例,但不等於以下為原始碼。
- 1、定義型別 T: 在型別 T 中必須帶有以 String name 為引數的 public 建構函式。例如:
BuildType.groovy
class BuildType {
// 必須有 String name 屬性,且不允許構造後修改
@Nonnull
public final String name
// 業務引數
boolean minifyEnabled
BuildType(String name) {
this.name = name
}
}
- 2、定義 NamedDomainObjectContainer 屬性: 在擴充套件類中定義一個 NamedDomainObjectContainer
型別屬性。例如:
CommonExtension.grooyv
class CommonExtension {
NamedDomainObjectContainer<BuildType> buildTypes
CommonExtension(Project project) {
// 通過 project.container(...) 方法建立 NamedDomainObjectContainer
NamedDomainObjectContainer<BuildType> buildTypeObjs = project.container(BuildType)
buildTypes = buildTypeObjs
}
// 巢狀擴充套件閉包函式,方法名為 buildTypes
void buildTypes(Action<NamedDomainObjectContainer<BuildType>> action) {
action.execute(buildTypes)
}
void buildTypes(Closure closure) {
ConfigureUtil.configure(closure, buildTypes)
}
}
- 3、建立 Extension: 按照 4.1 節介紹的步驟建立擴充套件。例如:
project.extensions.create("android", CommonExtension)
到這裡,就可以按照 buildTypes {} 的方式配置 BuildType 列表了。然而,你會發現每個配置項必須使用 =
進行賦值。這就有點膈應人了,有懂的大佬指導一下。
android {
buildTypes {
release {
// 怎樣才能省略 = 號呢?
minifyEnabled = false
proguardFiles = getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
4. 外掛除錯
4.1 兩個除錯方法
在開發外掛的過程一定需要除錯,除了通過日誌除錯,我們也有斷點除錯的需求。這裡總結兩個方法:方法 1 雖然只支援除錯簡單執行任務,但已經能滿足大部分需求,而且相對簡單。而方法 2 支援命令列新增引數。
方法 1(簡單): 直接提供 Android Studio 中 Gradle 皮膚的除錯功能,即可除錯外掛。如下圖,我們選擇與外掛功能相關的 Task,並右鍵選擇 Debug 執行。
方法 2: 通過配置 IDE Configuration 以支援除錯命令列任務,具體步驟:
- 1、建立 Remote 型別 Configuration:
- 2、執行命令:
./gradlew Task -Dorg.gradle.debug=true --no-daemon
(開啟 Debug & 不使用守護程式),執行後命令列會進入等待狀態:
- 3、Attach Debug: 點選除錯按鈕,即可開始斷點除錯。
4.2 除錯技巧
一些除錯技巧:
- 引用外掛原始碼: 在開發階段可以直接本地依賴外掛原始碼,而不需要將外掛釋出到 Maven 倉庫,只需要在 build.gradle 檔案中修改配置:
專案 build.gradle
buildscript {
repositories {
google()
jcenter()
}
dependencies {
// For Debug
classpath project(":easyupload")
// classpath "com.pengxr:easyupload:1.0.0"
}
...
}
- 外掛程式碼開關: 由於 Plugin#apply 中的程式碼在配置階段執行,如果其中的程式碼有問題就會出現 Sync 報錯。又因為編譯外掛程式碼需要先 Sync,只能先將工程中所有使用外掛的程式碼註釋掉,重新編譯外掛模組,再將註釋修改回來。真麻煩!我們還是加一個開關吧,例如:
gradle.properties
ENABLED=true
模組 build.gradle
if (ENABLED.toBoolean()) {
apply plugin: 'com.pengxr.easyupload'
upload {
name = "123"
}
}
5. 外掛開發技巧總結
- 判斷是否當前是 App 模組還是 Library 模組: 當我們開發 Android 專案相關外掛時,經常需要根據外掛的使用環境區分不同邏輯。例如外掛應用在 App 模組和 Library 模組會採用不同邏輯。此時,我們可以用在 Plugin#apply() 中採用以下判斷:
project.afterEvaluate {
// 1. Check if apply the ‘com.android.application’ plugin
if (!project.getPluginManager().hasPlugin("com.android.application")) {
return
}
}
- 外掛開發語言: 最初,Groovy 是 Gradle 的首要語言,但隨著 Java 和 Kotlin 語言的演進,這一現狀有所改變。現在的趨勢是:Gradle 指令碼使用 Groovy 或 Kotlin 開發,而 Gradle 外掛使用 Kotlin 開發。例如,我們可以發現 AGP 現在已經用 Kotlin 開發了。雖然趨勢是往 Kotlin 靠,但目前存量的 Gradle 指令碼 / 外掛還是以 Groovy 為主。
- Groovy 優勢:社群沉澱、動態語言
- Kotlin 優勢:IDE 支援、趨勢
原文: In general, a plugin implemented using Java or Kotlin, which are statically typed, will perform better than the same plugin implemented using Groovy.
6. 總結
到這裡,Gradle 外掛的部分就講完了,需要 Demo 的同學可以看下我們之前實現過的小外掛: EasyPrivacy。在本系列後續的文章中,也會有新的外掛 Demo。關注我,帶你瞭解更多,我們下次見。
參考資料
- 《實戰 Gradle》—— [美] Benjamin Muschko 著,李建 朱本威 楊柳 譯
- 《Gradle for Android》—— [美] Kevin Pelgrims 著,餘小樂 譯
- Groovy 參考文件 —— Groovy 官方文件
- Gradle 說明文件 —— Gradle 官方文件
- Gradle DSL 參考文件 —— Gradle 官方文件
- Developing Custom Gradle Plugins —— Gradle 官方文件
- Using Gradle Plugins —— Gradle 官方文件
- 深入探索 Gradle 自動化構建技術(系列) —— jsonchao 著