擁抱 Android Studio 之五:Gradle 外掛開發
實踐出真知
筆者有位朋友,每次新學一門語言,都會用來寫一個貪吃蛇遊戲,以此來檢驗自己學習的成果。筆者也有類似體會。所謂紙上得來終覺淺,絕知此事要躬行。這一章,筆者將以開發和釋出一個 Gradle 外掛作為目標,加深學習成果。
官方文件給出了比較詳細的實現步驟,本文的脈絡會跟官方文件差不了太多,額外增補實際例子和一些實踐經驗。文中的程式碼已經託管到了 github 專案中。
需求
預設的 Android 打包外掛會把 apk 命名成 module-productFlavor-buildType.apk
,例如 app-official-debug.apk
,並且會把包檔案釋出到固定的位置: module/build/outputs/apk
有的時候,這個命名風格並不是你所要的,你也想講 apk 輸出到別的目錄。我們們通過 gradle 外掛來實現自定義。這個外掛的需求是:
- 輸入一個名為 nameMap 的 Closure,用來修改 apk 名字
- 輸入一個名為 destDir 的 String,用於輸出位置
原理簡述
外掛之於 Gradle
根據官方文件定義,外掛打包了可重用的構建邏輯,可以適用於不同的專案和構建過程。
Gradle 提供了很多官方外掛,用於支援 Java、Groovy 等工程的構建和打包。同時也提供了自定義外掛的機制,讓每個人都可以通過外掛來實現特定的構建邏輯,並可以把這些邏輯打包起來,分享給其他人。
外掛的原始碼可以使用 Groovy、Scala、Java 三種語言,筆者不會 Scala,所以平時只是使用 Groovy 和 Java。前者用於實現與 Gradle 構建生命週期(如 task 的依賴)有關的邏輯,後者用於核心邏輯,表現為 Groovy 呼叫 Java 的程式碼。
另外,還有很多專案使用 Eclipse 或者 Maven 進行開發構建,用 Java 實現核心業務程式碼,將有利於實現快速遷移。
外掛打包方式
Gradle 的外掛有三種打包方式,主要是按照複雜程度和可見性來劃分:
Build script
把外掛寫在 build.gradle 檔案中,一般用於簡單的邏輯,只在該 build.gradle 檔案中可見,筆者常用來做原型除錯,本文將簡要介紹此類。
buildSrc 專案
將外掛原始碼放在 rootProjectDir/buildSrc/src/main/groovy
中,只對該專案中可見,適用於邏輯較為複雜,但又不需要外部可見的外掛,本文不介紹,有興趣可以參考此處。
獨立專案
一個獨立的 Groovy 和 Java 專案,可以把這個專案打包成 Jar 檔案包,一個 Jar 檔案包還可以包含多個外掛入口,將檔案包釋出到託管平臺上,供其他人使用。本文將著重介紹此類。
Build script 外掛
首先來直接在 build.gradle 中寫一個 plugin:
``` class ApkDistPlugin implements Plugin {
@Override
void apply(Project project) {
project.task('apkdist') << {
println 'hello, world!'
}
}
}
apply plugin: ApkDistPlugin ``` 命令列執行
$ ./gradlew -p app/ apkdist
:app:apkdist
hello, world!
這個外掛建立了一個名為 apkdist
的 task,並在 task 中列印。
外掛是一個類,繼承自 org.gradle.api.Plugin
介面,過載 void apply(Project project)
方法,這個方法將會傳入使用這個外掛的 project 的例項,這是一個重要的 context。
接受外部引數
通常情況下,外掛使用方需要傳入一些配置引數,如 bugtags 的 SDK 的外掛需要接受兩個引數:
bugtags {
appKey "APP_KEY" //這裡是你的 appKey
appSecret "APP_SECRET" //這裡是你的 appSecret,管理員在設定頁可以檢視
}
同樣,ApkDistPlugin 這個 plugin 也希望接受兩個引數:
apkdistconf {
nameMap { name ->
println 'hello,' + name
return name
}
destDir 'your-distribution-dir'
}
引數的內容後面繼續完善。那這兩個引數怎麼傳到外掛內呢?
org.gradle.api.Project
有一個 ExtensionContainer getExtensions()
方法,可以用來實現這個傳遞。
宣告引數類
宣告一個 Groovy 類,有兩個預設值為 null 的成員變數:
class ApkDistExtension {
Closure nameMap = null;
String destDir = null;
}
接受引數
project.extensions.create('apkdistconf', ApkDistExtension);
要注意,create
方法的第一個引數就是你在 build.gradle 檔案中的進行引數配置的 dsl 的名字,必須一致;第二個引數,就是引數類的名字。
獲取和使用引數
在 create 了 extension 之後,如果傳入了引數,則會攜帶在 project 例項中,
``` def closure = project['apkdistconf'].nameMap; closure('wow!');
println project['apkdistconf'].destDir ```
進化版本一:引數
``` class ApkDistExtension { Closure nameMap = null; String destDir = null; }
class ApkDistPlugin implements Plugin {
@Override
void apply(Project project) {
project.extensions.create('apkdistconf', ApkDistExtension);
project.task('apkdist') << {
println 'hello, world!'
def closure = project['apkdistconf'].nameMap;
closure('wow!');
println project['apkdistconf'].destDir
}
}
}
apply plugin: ApkDistPlugin
apkdistconf { nameMap { name -> println 'hello, ' + name return name } destDir 'your-distribution-directory' }
```
執行結果:
``` $ ./gradlew -p app/ apkdist :app:apkdist hello, world! hello, wow! your-distribution-directory
```
獨立專案外掛
程式碼寫到現在,已經不適合再放在一個 build.gradle 檔案裡面了,那也不是我們的目的。建立一個獨立專案,把程式碼搬到對應的地方。
理論上,IntelliJ IDEA 開發外掛要比 Android Studio 要方便一點點,因為有對應 Groovy module 的模板。但其實如果我們瞭解 IDEA 的專案檔案結構,就不會受到這個侷限,無非就是一個 build.gradle 構建檔案加 src 原始碼資料夾。
最終專案的資料夾結構是這樣:
下面我們來一步步講解。
建立專案
在 Android Studio 中新建 Java Library
module “plugin”
。
修改 build.gradle 檔案
新增 Groovy 外掛和對應的兩個依賴。
``` //removed java plugin apply plugin: 'groovy'
dependencies { compile gradleApi()//gradle sdk compile localGroovy()//groovy sdk compile fileTree(dir: 'libs', include: ['*.jar']) } ```
修改專案資料夾
src/main 專案檔案下:
- 移除 java 資料夾,因為在這個專案中用不到 java 程式碼
- 新增 groovy 資料夾,主要的程式碼檔案放在這裡
- 新增 resources 資料夾,存放用於標識 gradle 外掛的 meta-data
建立對應檔案
``` . ├── build.gradle ├── libs ├── plugin.iml └── src └── main ├── groovy │ └── com │ └── asgradle │ └── plugin │ ├── ApkDistExtension.groovy │ └── ApkDistPlugin.groovy └── resources └── META-INF └── gradle-plugins └── com.asgradle.apkdist.properties
```
注意:
- groovy 資料夾中的類,一定要修改成
.groovy
字尾,IDE 才會正常識別。 - resources/META-INF/gradle-plugins 這個資料夾結構是強制要求的,否則不能識別成外掛。
com.asgradle.apkdist.properties 檔案
如果寫過 Java 的同學會知道,這是一個 Java 的 properties 檔案,是 key=value
的格式。這個檔案內容如下:
implementation-class=com.asgradle.plugin.ApkDistPlugin
按其語義推斷,是指定這個外掛的入口類。
- 英文敏感的同學可能會問了,為什麼這個檔案的承載資料夾是叫做
gradle-plugins
,使用複數?沒錯,這裡可以指定多個 properties 檔案,定義多個外掛,擴充套件性一流,可以參考 linkedin 的外掛的組織方式。 使用這個外掛的時候,將會是這樣:
apply plugin:'com.asgradle.apkdist'
因此,com.asgradle.apkdist
這個字串在這裡,又稱為這個外掛的 id,不允許跟別的外掛重複,取你擁有的域名的反向就不會錯。
將 plugin module 傳到本地 maven 倉庫
參考上一篇:擁抱 Android Studio 之四:Maven 倉庫使用與私有倉庫搭建,和對應的 demo 專案,將包傳到本地倉庫中進行測試。
新增 gradle.properties
``` PROJ_NAME=gradleplugin PROJ_ARTIFACTID=gradleplugin PROJ_POM_NAME=Local Repository
LOCAL_REPO_URL=file:///Users/changbinhe/Documents/Android/repo/
PROJ_GROUP=com.as-gradle.demo
PROJ_VERSION=1.0.0 PROJ_VERSION_CODE=1
PROJ_WEBSITEURL=http://kvh.io PROJ_ISSUETRACKERURL=https://github.com/kevinho/Embrace-Android-Studio-Demo/issues PROJ_VCSURL=https://github.com/kevinho/Embrace-Android-Studio-Demo.git PROJ_DESCRIPTION=demo apps for embracing android studio
PROJ_LICENCE_NAME=The Apache Software License, Version 2.0 PROJ_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt PROJ_LICENCE_DEST=repo
DEVELOPER_ID=your-dev-id DEVELOPER_NAME=your-dev-name DEVELOPER_EMAIL=your-email@your-mailbox.com ```
在 build.gradle 新增上傳功能
``` apply plugin: 'maven'
uploadArchives { repositories.mavenDeployer { repository(url: LOCAL_REPO_URL) pom.groupId = PROJ_GROUP pom.artifactId = PROJ_ARTIFACTID pom.version = PROJ_VERSION } }
```
上傳可以通過執行:
``` $ ./gradlew -p plugin/ clean build uploadArchives
```
在 app module 中使用外掛
在專案的 buildscript 新增外掛作為 classpath
buildscript {
repositories {
maven{
url 'file:///Users/your-user-name/Documents/Android/repo/'
}
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.1.0-alpha3'
classpath 'com.as-gradle.demo:gradleplugin:1.0.0'
}
}
在 app module 中使用外掛:
apply plugin: 'com.asgradle.apkdist'
命令列執行:
$ ./gradlew -p app apkdist
:app:apkdist
hello, world!
hello, wow!
your-distribution-directory
可能會遇到問題
Error:(46, 0) Cause: com/asgradle/plugin/ApkDistPlugin : Unsupported major.minor version 52.0
<a href="openFile:/Users/your-user-name/Documents/git/opensource/embrace-android-studio-demo/s5-GradlePlugin/app/build.gradle">Open File</a>
應該是本機的 JDK 版本是1.8,預設將 plugin module 的 groovy 原始碼編譯成了1.8版本的 class 檔案,放在 Android 專案中,無法相容。需要對 plugin module 的 build.gradle 檔案新增兩個引數:
sourceCompatibility = 1.6
targetCompatibility = 1.6
真正的實現外掛需求
讀者可能會觀察到,到目前為止,外掛只是跑通了流程,並沒有實現本文提出的兩個需求,
那接下來就具體實現一下。
``` class ApkDistPlugin implements Plugin {
@Override
void apply(Project project) {
project.extensions.create('apkdistconf', ApkDistExtension);
project.afterEvaluate {
//只可以在 android application 或者 android lib 專案中使用
if (!project.android) {
throw new IllegalStateException('Must apply \'com.android.application\' or \'com.android.library\' first!')
}
//配置不能為空
if (project.apkdistconf.nameMap == null || project.apkdistconf.destDir == null) {
project.logger.info('Apkdist conf should be set!')
return
}
Closure nameMap = project['apkdistconf'].nameMap
String destDir = project['apkdistconf'].destDir
//列舉每一個 build variant
project.android.applicationVariants.all { variant ->
variant.outputs.each { output ->
File file = output.outputFile
output.outputFile = new File(destDir, nameMap(file.getName()))
}
}
}
}
} ```
必須指出,本文外掛實現的需求,其實可以直接在 app module 的 build.gradle 中寫指令碼就可以實現。這裡做成外掛,只是為了做示範。
上傳到 bintray 的過程,就不再贅述了,可以參考擁抱 Android Studio 之四:Maven 倉庫使用與私有倉庫搭建。
後記
至此,這系列開篇的時候挖下的坑,終於填完了。很多人藉助這系列的講解,真正理解了 Android Studio 和它背後的 Gradle、Groovy,筆者十分高興。筆者也得到了很多讀者的鼓勵和支援,心中十分感激。
寫部落格真的是一個很講究執行力和耐力的事情,但既然挖下了坑,就得填上,對吧?
這半年來,個人在 Android 和 Java 平臺上也做了更多的事情,也有了更多的體會。
AS 系列,打算擴充幾個主題:
- Proguard 混淆
- Java & Android Testing
- Maven 私有倉庫深入
- 持續整合
- ……待發掘
記得有人說,只懂 Android 不懂 Java,是很可怕的。在這半年以來,筆者在工作中使用 Java 實現了一些後端服務,也認真學習了 JVM 位元組碼相關的知識並把它使用到了工作中。在這個過程中,真的很為 Java 平臺的活力、豐富的庫資源、幾乎無止境的可能性所折服。接下來,會寫一些跟有關的學習體會,例如:
- Java 多執行緒與鎖
- JVM 部分原理
- 位元組碼操作
- Java 8部分特性
- ……待學習
隨著筆者工作的進展,我也有機會學習使用了別的語言,例如 Node.js,並實現了一些後端服務。這個語言的活力很強,一些比 Java 現代的地方,很吸引人。有精力會寫一寫。
因為業務所需,筆者所經歷的系統,正在處於像面向服務的演化過程中,我們期望建立統一的通訊平臺和規範,抽象系統的資源,拆分業務,容器化。這是一個很有趣的過程,也是對我們的挑戰。筆者也希望有機會與讀者分享。
一不小心又挖下了好多明坑和無數暗坑,只是為了激勵自己不斷往前。在探索事物本質的旅途中,必然十分艱險,又十分有趣,沿途一定風光絢麗,讓我們共勉。
參考文獻
系列導讀
本文是筆者《擁抱 Android Studio》系列第四篇,其他篇請點選:
擁抱 Android Studio 之一:從 ADT 到 Android Studio
擁抱 Android Studio 之二:Android Studio 與 Gradle 深入
擁抱 Android Studio 之三:溯源,Groovy 與 Gradle 基礎
擁抱 Android Studio 之四:Maven 公共倉庫使用與私有倉庫搭建
擁抱 Android Studio 之五:Gradle 外掛使用與開發
有問題?在文章下留言或者加 qq 群:453503476,希望能幫到你。
番外
筆者 kvh 在開發和運營 bugtags.com,這是一款移動時代首選的 bug 管理系統,能夠極大的提升 app 開發者的測試效率,歡迎使用、轉發推薦。
筆者目前關注點在於移動 SDK 研發,後端服務設計和實現。
我們團隊長期求 PHP 後端研發,有興趣請加下面公眾號勾搭:
相關文章
- Android Studio之Gradle和Gradle外掛的區別AndroidGradle
- Android Studio 外掛開發Android
- Android與Gradle(一):Gradle外掛開發AndroidGradle
- 使用android studio開發gradle外掛(三)-讀取配置檔案AndroidGradle
- Android Studio gradle外掛版本和gradle版本對應關係AndroidGradle
- Android Gradle外掛AndroidGradle
- 使用 Java 開發 Gradle 外掛JavaGradle
- Android Studio Plugin 外掛開發教程(一) —— 開發你的第一個外掛AndroidPlugin
- Gradle系列之Gradle外掛Gradle
- Android開發中Gradle外掛,Gradle版本與JDK版本之間的對應關係 AndroidAndroidGradleJDK
- IntelliJ IDEA/Android Studio外掛開發指南IntelliJIdeaAndroid
- Android Studio 讓開發效率事半功倍的外掛整理Android
- 高效率開發的Android Studio外掛收集Android
- Android Studio外掛Android
- android studio 外掛Android
- Android Studio Plugin 外掛開發教程(二) —— 外掛SDK中的常用物件介紹AndroidPlugin物件
- 從頭開始開發一個 Gradle 外掛Gradle
- 不得不學之「 Gradle」 ⑤ Gradle 外掛Gradle
- Gradle入門系列(五)——Gradle其它模組與Plugin外掛GradlePlugin
- 前端開發值得擁有的 VSCode 外掛前端VSCode
- Android Studio NDK :一、基礎入門(基於gradle-experimental外掛)AndroidGradle
- Spring Boot 把 Maven 幹掉了,擁抱 Gradle!Spring BootMavenGradle
- Flutter開發之Flutter外掛開發Flutter
- Flutter外掛開發---Android篇FlutterAndroid
- Android外掛式開發(一)Android
- Gradle核心思想(五)通俗易懂的Gradle外掛講解Gradle
- Gradle系列(四) Gradle外掛Gradle
- 擁抱ES6——OSSJavaScriptSDK開發手記JavaScript
- Android Studio Plugin 外掛開發教程(四) —— 為自動生成資料庫程式碼的外掛新增UIAndroidPlugin資料庫UI
- 打造你的開發神器——介紹Android Studio上的幾個外掛Android
- Android Studio 中如何引入ButterKnife外掛Android
- 扔掉 Electron,擁抱基於 Rust 開發的 TauriRust
- gradle自定義外掛Gradle
- Android Studio Plugin 外掛開發教程(三) —— 製作一個自動生成資料庫程式碼的外掛AndroidPlugin資料庫
- 擁抱開源,從中受益
- Gradle for Android系列之五 多渠道打包GradleAndroid
- Visual Studio 必備外掛集合:AI 助力開發AI
- 擁抱 NamedParameterJdbcTemplateJDBC