實(shí)踐出真知
筆者有位朋友,每次新學(xué)一門語(yǔ)言,都會(huì)用來(lái)寫一個(gè)貪吃蛇游戲,以此來(lái)檢驗(yàn)自己學(xué)習(xí)的成果。筆者也有類似體會(huì)。所謂紙上得來(lái)終覺(jué)淺,絕知此事要躬行。這一章,筆者將以開發(fā)和發(fā)布一個(gè) Gradle 插件作為目標(biāo),加深學(xué)習(xí)成果。
官方文檔給出了比較詳細(xì)的實(shí)現(xiàn)步驟,本文的脈絡(luò)會(huì)跟官方文檔差不了太多,額外增補(bǔ)實(shí)際例子和一些實(shí)踐經(jīng)驗(yàn)。文中的代碼已經(jīng)托管到了 github 項(xiàng)目中。
需求
默認(rèn)的 Android 打包插件會(huì)把 apk 命名成module-productFlavor-buildType.apk,例如app-official-debug.apk,并且會(huì)把包文件發(fā)布到固定的位置:module/build/outputs/apk有的時(shí)候,這個(gè)命名風(fēng)格并不是你所要的,你也想講 apk 輸出到別的目錄。咱們通過(guò) gradle 插件來(lái)實(shí)現(xiàn)自定義。這個(gè)插件的需求是:
輸入一個(gè)名為 nameMap 的 Closure,用來(lái)修改 apk 名字 輸入一個(gè)名為 destDir 的 String,用于輸出位置 原理簡(jiǎn)述 插件之于 Gradle
根據(jù)官方文檔定義,插件打包了可重用的構(gòu)建邏輯,可以適用于不同的項(xiàng)目和構(gòu)建過(guò)程。
Gradle 提供了很多官方插件,用于支持 Java、Groovy 等工程的構(gòu)建和打包。同時(shí)也提供了自定義插件的機(jī)制,讓每個(gè)人都可以通過(guò)插件來(lái)實(shí)現(xiàn)特定的構(gòu)建邏輯,并可以把這些邏輯打包起來(lái),分享給其他人。
插件的源碼可以使用 Groovy、Scala、Java 三種語(yǔ)言,筆者不會(huì) Scala,所以平時(shí)只是使用 Groovy 和 Java。前者用于實(shí)現(xiàn)與 Gradle 構(gòu)建生命周期(如 task 的依賴)有關(guān)的邏輯,后者用于核心邏輯,表現(xiàn)為 Groovy 調(diào)用 Java 的代碼。
另外,還有很多項(xiàng)目使用 Eclipse 或者 Maven 進(jìn)行開發(fā)構(gòu)建,用 Java 實(shí)現(xiàn)核心業(yè)務(wù)代碼,將有利于實(shí)現(xiàn)快速遷移。
插件打包方式
Gradle 的插件有三種打包方式,主要是按照復(fù)雜程度和可見(jiàn)性來(lái)劃分:
Build script
把插件寫在 build.gradle 文件中,一般用于簡(jiǎn)單的邏輯,只在該 build.gradle 文件中可見(jiàn),筆者常用來(lái)做原型調(diào)試,本文將簡(jiǎn)要介紹此類。
buildSrc 項(xiàng)目
將插件源代碼放在rootProjectDir/buildSrc/src/main/groovy中,只對(duì)該項(xiàng)目中可見(jiàn),適用于邏輯較為復(fù)雜,但又不需要外部可見(jiàn)的插件,本文不介紹,有興趣可以參考此處。
獨(dú)立項(xiàng)目
一個(gè)獨(dú)立的 Groovy 和 Java 項(xiàng)目,可以把這個(gè)項(xiàng)目打包成 Jar 文件包,一個(gè) Jar 文件包還可以包含多個(gè)插件入口,將文件包發(fā)布到托管平臺(tái)上,供其他人使用。本文將著重介紹此類。
Build script 插件
首先來(lái)直接在 build.gradle 中寫一個(gè) plugin:
class ApkDistPlugin implements Plugin { @Override void apply(Project project) { project.task(\'apkdist\') << { println \'hello, world!\' } } } apply plugin: ApkDistPlugin
命令行運(yùn)行
$ ./gradlew -p app/ apkdist :app:apkdist hello, world!
這個(gè)插件創(chuàng)建了一個(gè)名為apkdist的 task,并在 task 中打印。
插件是一個(gè)類,繼承自org.gradle.api.Plugin接口,重載void apply(Project project)方法,這個(gè)方法將會(huì)傳入使用這個(gè)插件的 project 的實(shí)例,這是一個(gè)重要的 context。
接受外部參數(shù)
通常情況下,插件使用方需要傳入一些配置參數(shù),如 bugtags 的 SDK 的插件需要接受兩個(gè)參數(shù):
bugtags { appKey "APP_KEY" //這里是你的 appKey appSecret "APP_SECRET" //這里是你的 appSecret,管理員在設(shè)置頁(yè)可以查看 }
同樣,ApkDistPlugin 這個(gè) plugin 也希望接受兩個(gè)參數(shù):
apkdistconf { nameMap { name -> println \'hello,\' + name return name } destDir \'your-distribution-dir\' }
參數(shù)的內(nèi)容后面繼續(xù)完善。那這兩個(gè)參數(shù)怎么傳到插件內(nèi)呢?
org.gradle.api.Project有一個(gè)ExtensionContainer getExtensions()方法,可以用來(lái)實(shí)現(xiàn)這個(gè)傳遞。
聲明參數(shù)類
聲明一個(gè) Groovy 類,有兩個(gè)默認(rèn)值為 null 的成員變量:
class ApkDistExtension { Closure nameMap = null; String destDir = null; } 接受參數(shù)
project.extensions.create(\'apkdistconf\', ApkDistExtension);
要注意,create方法的第一個(gè)參數(shù)就是你在 build.gradle 文件中的進(jìn)行參數(shù)配置的 dsl 的名字,必須一致;第二個(gè)參數(shù),就是參數(shù)類的名字。
獲取和使用參數(shù)
在 create 了 extension 之后,如果傳入了參數(shù),則會(huì)攜帶在 project 實(shí)例中,
def closure = project[\'apkdistconf\'].nameMap; closure(\'wow!\'); println project[\'apkdistconf\'].destDir 進(jìn)化版本一:參數(shù)
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\' }
運(yùn)行結(jié)果:
$ ./gradlew -p app/ apkdist :app:apkdist hello, world! hello, wow! your-distribution-directory 獨(dú)立項(xiàng)目插件
代碼寫到現(xiàn)在,已經(jīng)不適合再放在一個(gè) build.gradle 文件里面了,那也不是我們的目的。建立一個(gè)獨(dú)立項(xiàng)目,把代碼搬到對(duì)應(yīng)的地方。
理論上,IntelliJ IDEA 開發(fā)插件要比 Android Studio 要方便一點(diǎn)點(diǎn),因?yàn)橛袑?duì)應(yīng) Groovy module 的模板。但其實(shí)如果我們了解 IDEA 的項(xiàng)目文件結(jié)構(gòu),就不會(huì)受到這個(gè)局限,無(wú)非就是一個(gè) build.gradle 構(gòu)建文件加 src 源碼文件夾。
最終項(xiàng)目的文件夾結(jié)構(gòu)是這樣:
Java-Library
下面我們來(lái)一步步講解。
創(chuàng)建項(xiàng)目
在 Android Studio 中新建Java Librarymodule“plugin”。
修改 build.gradle 文件
添加 Groovy 插件和對(duì)應(yīng)的兩個(gè)依賴。
//removed java plugin apply plugin: \'groovy\' dependencies { compile gradleApi()//gradle sdk compile localGroovy()//groovy sdk compile fileTree(dir: \'libs\', include: [\'*.jar\']) } 修改項(xiàng)目文件夾
src/main 項(xiàng)目文件下:
移除 java 文件夾,因?yàn)樵谶@個(gè)項(xiàng)目中用不到 java 代碼 添加 groovy 文件夾,主要的代碼文件放在這里 添加 resources 文件夾,存放用于標(biāo)識(shí) gradle 插件的 meta-data 建立對(duì)應(yīng)文件
. ├── 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 才會(huì)正常識(shí)別。 resources/META-INF/gradle-plugins 這個(gè)文件夾結(jié)構(gòu)是強(qiáng)制要求的,否則不能識(shí)別成插件。 com.asgradle.apkdist.properties 文件
如果寫過(guò) Java 的同學(xué)會(huì)知道,這是一個(gè) Java 的 properties 文件,是key=value的格式。這個(gè)文件內(nèi)容如下:
implementation-class=com.asgradle.plugin.ApkDistPlugin
按其語(yǔ)義推斷,是指定這個(gè)插件的入口類。
英文敏感的同學(xué)可能會(huì)問(wèn)了,為什么這個(gè)文件的承載文件夾是叫做gradle-plugins,使用復(fù)數(shù)?沒(méi)錯(cuò),這里可以指定多個(gè) properties 文件,定義多個(gè)插件,擴(kuò)展性一流,可以參考 linkedin 的插件的組織方式。
使用這個(gè)插件的時(shí)候,將會(huì)是這樣:
apply plugin:\'com.asgradle.apkdist\'
因此,com.asgradle.apkdist這個(gè)字符串在這里,又稱為這個(gè)插件的 id,不允許跟別的插件重復(fù),取你擁有的域名的反向就不會(huì)錯(cuò)。
將 plugin module 傳到本地 maven 倉(cāng)庫(kù)
參考上一篇:擁抱 Android Studio 之四:Maven 倉(cāng)庫(kù)使用與私有倉(cāng)庫(kù)搭建,和對(duì)應(yīng)的 demo 項(xiàng)目,將包傳到本地倉(cāng)庫(kù)中進(jìn)行測(cè)試。
添加 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 } }
上傳可以通過(guò)運(yùn)行:
$ ./gradlew -p plugin/ clean build uploadArchives 在 app module 中使用插件 在項(xiàng)目的 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\'
命令行運(yùn)行:
$ ./gradlew -p app apkdist :app:apkdist hello, world! hello, wow! your-distribution-directory 可能會(huì)遇到問(wèn)題
Error:(46, 0) Cause: com/asgradle/plugin/ApkDistPlugin : Unsupported major.minor version 52.0 Open File
應(yīng)該是本機(jī)的 JDK 版本是1.8,默認(rèn)將 plugin module 的 groovy 源碼編譯成了1.8版本的 class 文件,放在 Android 項(xiàng)目中,無(wú)法兼容。需要對(duì) plugin module 的 build.gradle 文件添加兩個(gè)參數(shù):
sourceCompatibility = 1.6 targetCompatibility = 1.6 真正的實(shí)現(xiàn)插件需求
讀者可能會(huì)觀察到,到目前為止,插件只是跑通了流程,并沒(méi)有實(shí)現(xiàn)本文提出的兩個(gè)需求,
那接下來(lái)就具體實(shí)現(xiàn)一下。
class ApkDistPlugin implements Plugin { @Override void apply(Project project) { project.extensions.create(\'apkdistconf\', ApkDistExtension); project.afterEvaluate { //只可以在 android application 或者 android lib 項(xiàng)目中使用 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 //枚舉每一個(gè) build variant project.android.applicationVariants.all { variant -> variant.outputs.each { output -> File file = output.outputFile output.outputFile = new File(destDir, nameMap(file.getName())) } } } } }
必須指出,本文插件實(shí)現(xiàn)的需求,其實(shí)可以直接在 app module 的 build.gradle 中寫腳本就可以實(shí)現(xiàn)。這里做成插件,只是為了做示范。
上傳到 bintray 的過(guò)程,就不再贅述了,可以參考擁抱 Android Studio 之四:Maven 倉(cāng)庫(kù)使用與私有倉(cāng)庫(kù)搭建。
后記
至此,這系列開篇的時(shí)候挖下的坑,終于填完了。很多人借助這系列的講解,真正理解了 Android Studio 和它背后的 Gradle、Groovy,筆者十分高興。筆者也得到了很多讀者的鼓勵(lì)和支持,心中十分感激。
寫博客真的是一個(gè)很講究執(zhí)行力和耐力的事情,但既然挖下了坑,就得填上,對(duì)吧?
這半年來(lái),個(gè)人在 Android 和 Java 平臺(tái)上也做了更多的事情,也有了更多的體會(huì)。
AS 系列,打算擴(kuò)充幾個(gè)主題:
Proguard 混淆 Java & Android Testing Maven 私有倉(cāng)庫(kù)深入 持續(xù)集成 ……待發(fā)掘
記得有人說(shuō),只懂 Android 不懂 Java,是很可怕的。在這半年以來(lái),筆者在工作中使用 Java 實(shí)現(xiàn)了一些后端服務(wù),也認(rèn)真學(xué)習(xí)了 JVM 字節(jié)碼相關(guān)的知識(shí)并把它使用到了工作中。在這個(gè)過(guò)程中,真的很為 Java 平臺(tái)的活力、豐富的庫(kù)資源、幾乎無(wú)止境的可能性所折服。接下來(lái),會(huì)寫一些跟有關(guān)的學(xué)習(xí)體會(huì),例如:
Java 多線程與鎖 JVM 部分原理 字節(jié)碼操作 Java 8部分特性 ……待學(xué)習(xí)
隨著筆者工作的進(jìn)展,我也有機(jī)會(huì)學(xué)習(xí)使用了別的語(yǔ)言,例如 Node.js,并實(shí)現(xiàn)了一些后端服務(wù)。這個(gè)語(yǔ)言的活力很強(qiáng),一些比 Java 現(xiàn)代的地方,很吸引人。有精力會(huì)寫一寫。
因?yàn)闃I(yè)務(wù)所需,筆者所經(jīng)歷的系統(tǒng),正在處于像面向服務(wù)的演化過(guò)程中,我們期望建立統(tǒng)一的通訊平臺(tái)和規(guī)范,抽象系統(tǒng)的資源,拆分業(yè)務(wù),容器化。這是一個(gè)很有趣的過(guò)程,也是對(duì)我們的挑戰(zhàn)。筆者也希望有機(jī)會(huì)與讀者分享。
一不小心又挖下了好多明坑和無(wú)數(shù)暗坑,只是為了激勵(lì)自己不斷往前。在探索事物本質(zhì)的旅途中,必然十分艱險(xiǎn),又十分有趣,沿途一定風(fēng)光絢麗,讓我們共勉。
參考文獻(xiàn)
官方文檔
系列導(dǎo)讀
本文是筆者《擁抱 Android Studio》系列第四篇,其他篇請(qǐng)點(diǎn)擊:
擁抱 Android Studio 之一:從 ADT 到 Android Studio
擁抱 Android Studio 之二:Android Studio 與 Gradle 深入
擁抱 Android Studio 之三:溯源,Groovy 與 Gradle 基礎(chǔ)
擁抱 Android Studio 之四:Maven 公共倉(cāng)庫(kù)使用與私有倉(cāng)庫(kù)搭建
擁抱 Android Studio 之五:Gradle 插件使用與開發(fā)
有問(wèn)題?在文章下留言或者加 qq 群:453503476,希望能幫到你。
番外
筆者 kvh 在開發(fā)和運(yùn)營(yíng) bugtags.com,這是一款移動(dòng)時(shí)代選擇的 bug 管理系統(tǒng),能夠極大的提升 app 開發(fā)者的測(cè)試效率,歡迎使用、轉(zhuǎn)發(fā)推薦。
筆者目前關(guān)注點(diǎn)在于移動(dòng) SDK 研發(fā),后端服務(wù)設(shè)計(jì)和實(shí)現(xiàn)。