在上一節《補齊Android技能樹 - 玩轉Gradle(二)》 提到過插件,有下面這樣一段話:html
Gradle自身 並無提供編譯打包的功能,它只是一個 負責定義流程和規則的框架,具體的編譯工做都是由 插件 來完成的,好比編譯Java用Java插件,編譯Kotlin用Kotlin插件。插件的本質就是:定義Task,並具體執行這些Task的模板。java
本節就來了解下:Gradle插件編寫的知識儲備
及 插件發佈相關
姿式,讀者亦可自行查閱官方文檔:Packaging a pluginandroid
Gradle插件本質上是一個jar文件,能夠用idea建立項目,也可使用gradle init命令建立,示例以下:git
Tips:選擇Groovy、Java、Kotlin實現均可,腳本DSL選Groovy、Kotlin皆可,筆者都用的Groovy ~github
嚴格來講不上插件,就是把構建腳本代碼寫到一個單獨的文件中,複製粘貼到項目目錄下引用。示例以下:web
外部構建腳本:other.gradleapi
// 腳本內部訪問
def versionName = "v1.0.0"
def versionDesc = "第一個版本"
// 腳本外部訪問
ext {
author = "CoderPig"
}
// Task任務
task printVersionInfo {
doLast {
println "$versionName → $versionDesc"
}
}
複製代碼
build.gradle中引用此腳本:markdown
apply from: 'other.gradle'
task test {
dependsOn(printVersionInfo)
doFirst { println(author) }
doLast { println("build.gradle裏的task") }
}
複製代碼
鍵入 gradle test
執行任務結果輸出以下:閉包
執行Gradle時會將根目錄下的buildSrc目錄做爲插件源碼目錄進行編譯,並將結果加入到構建腳本的classpath中。app
這種插件不須要plugins{}引入,通常適合沒有複用性的插件或者新插件開發調試用,還有個缺點:沒法使用屬性配置DSL,需經過 configure<...> {...}
配置插件屬性。
將項目打成jar包,可在多個項目間複用,通常的插件開發都是指的這一類~
咱們上面經過命令行建立了一個Gradle插件項目,接着來康一康都有哪些核心要素( 順帶吐槽下網上一堆說得不清不楚的文章...)
① 插件實現類:CpPluginPlugin.groovy
package cn.coderpig.plugins
import org.gradle.api.Project
import org.gradle.api.Plugin
public class CpPluginPlugin implements Plugin<Project> {
public void apply(Project project) {
// 註冊一個Task
project.tasks.register("greeting") {
doLast {
println("Hello from plugin 'cn.coderpig.plugins.greeting'")
}
}
}
}
複製代碼
代碼比較淺顯易懂,實現了 Plugin
接口,指定泛型類爲Project,定義了apply方法,並註冊了一個名爲greeting帶閉包的Task,打印一句話。
② 根目錄下的 build.gradle
// 引用java gradle插件開發插件、groovy支持
plugins {
id 'java-gradle-plugin'
id 'groovy'
}
gradlePlugin {
plugins {
greeting {
// 插件id
id = 'cn.coderpig.plugins.greeting'
// 插件實現類
implementationClass = 'cn.coderpig.plugins.CpPluginPlugin'
}
}
}
複製代碼
Tips:網上不少Gradle插件開發教程還要另外配置一個 properties 文件,如:
src/main/resources/META-INF/gradle-plugins/cn.coderpig.plugins.greeting.properties
文件內容以下:
implementation-class=cn.coderpig.plugins.CpPluginPlugin
實際上,在build.gradle中聲明瞭gradlePlugin就能夠了,無需另外再配置一遍!
使用插件可使用上面的buildSrc方式引入,也能夠先打成jar包,自用就發佈到本地,分享給別人用就發佈到Maven或者JCenter倉庫。先試試發佈到本地吧,須要添加Maven相關的配置:
plugins {
// 添加maven插件
id 'maven'
}
uploadArchives {
repositories.mavenDeployer {
repository(url: uri('C:\\Users\\用戶名\\Maven\\repo')) // 本地倉庫路徑
pom.groupId = "cn.coderpig.plugins"// 惟一標識(一般爲模塊包名,也能夠任意)
pom.artifactId = "CpPluginPlugin" // 項目名稱(一般爲類庫模塊名稱,也能夠任意)
pom.version = "0.0.1" // 版本號
}
}
複製代碼
配置完Sync Now從新構建下項目,在Gradle窗口就會多出一個uploda目錄,裏面的 uploadArchives
就是將插件類庫發佈到倉庫的Task:
雙擊執行此task,在 C:\Users\用戶名\Maven\repo 下生成下述文件:
發佈到本地Maven後,就能夠在另外一個項目中引用驗證插件效果,先修改根目錄的 build.gradle
:
buildscript {
repositories {
...
// 本地Maven地址
maven { url 'C:\\Users\\用戶名\\Maven\\repo' }
}
dependencies {
...
// 插件依賴
classpath "cn.coderpig.plugins:cpplugin:0.0.1"
}
}
複製代碼
app目錄或module目錄的 build.gradle
引用此插件:
plugins {
...
id 'cn.coderpig.plugins.greeting'
}
複製代碼
接着寫一個Task來驗證下:
task("testPlugin") {
group("custom") // 分組,方便找到Task
dependsOn('greeting') // 調用插件裏的greeting Task,在執行testPlugin
doLast { println '任務執行完畢' }
}
複製代碼
運行結果以下:
說到插件遠程發佈,網上十有八九的教程都是傳到 JFlog Bintray,打開官網倒是大紅提示:
其實早在今年的2.3,官方就發佈了一則通告:
包括 GoCenter、Bintray、JCenter 在內的多項軟件包管理和分發服務都將中止運營。
自3.31後就不在接受任何新的提交,在2022.2.1前,你仍是能夠正常拉取2021.3.31前提交的庫。
不能提交的話就只能找找JCenter的替代品咯,由於這裏編寫的是Gradle插件,能夠試下提交到Gradle的遠程倉庫。
完整流程介紹可參見官方文檔:How do I add my plugin to the plugin portal?,這裏簡述下步驟:
先註冊個帳號 (Github受權登陸亦可),登陸後點擊生成API Keys:
複製粘貼到本地gradle配置文件中:HOME_DIR/.gradle/gradle.properties (~/.gradle/gradle.properties)
接着照着:How do I use the Plugin Publishing Plugin? 配置下com.gradle.plugin-publish上傳插件須要的一些參數:
plugins {
...
id "com.gradle.plugin-publish" version "0.14.0" // 上傳插件
}
version = "0.0.1" // 自定義插件版本
group = "cn.coderpig.plugins" // 自定義插件分組
// 自定義插件id及實現類
gradlePlugin {
plugins {
greeting {
id = 'cn.coderpig.plugins.greeting'
implementationClass = 'cn.coderpig.plugins.CpPluginPlugin'
}
}
}
// 插件附加信息
pluginBundle {
website = 'https://github.com/coderpig/cpplugin'
vcsUrl = 'https://github.com/coderpig/cpplugin'
description = 'cpplugin gradle plugin' //插件描述
tags = ['cp'] //搜索關鍵詞
plugins {
greeting {
// id會從插件java-gradle-plugin處自動獲取
displayName = 'cpplugin gradle plugin'
}
}
}
複製代碼
配置完Sync Now從新構建下項目,在Gradle窗口就會多出一個plugin portal目錄,點擊 publishPlugins
便可發佈插件到Gradle:
發佈成功後須要等待官方審覈,審覈經過的話就能夠在官方搜索到本身的插件了,固然我這種亂寫和信息亂填的Demo確定是過不了審的,2333,只是演示下流程~
Gradle Plugin倉庫只適合Gradle插件發佈,平常用第三方庫可不支持,順帶提提另外兩個方案,先說說 JitPack。
基於Github倉庫的發佈倉庫,發佈方式也不復雜,照着官方用戶指南走:使用 Maven Publish 插件
在你的庫的 build.gradle
文件中增長對應配置信息,接着push到Github上,點擊Releases面板 → create new release,依次輸入版本號、標題和描述,而後點擊Publish release便可。
接着回到JitPack,定位到本身寫的庫,而後四個tab,選中Releases → 對應版本點下Get it,接着靜待片刻:
這裏的紅色表明編譯失敗,失敗的緣由是這個庫沒有添加相關配置,正常編譯經過是綠色的,而後下方能夠看到如何在項目中依賴這個庫,Tag替換成對應版本,好比這裏的1.0.2 :
上傳庫到MavenCentral前須要註冊登陸:Sonatype,進入網頁後點擊Sign up進入註冊頁面註冊:
擁有Sonatype帳號後,點開 管理後臺,Log In下,會彈下述錯誤提示框:
須要申請一波Sonatype上傳權限,回到 issues.sonatype.org/ 頁,點擊新建,填寫項目信息:
提交完等審覈吧,通常會讓你證實域名真的是你本身的:
第一個種解決方式最簡單,域名DNS坐下解析,添加一個TXT類型的記錄便可,如:
接着在官網那裏回覆下他這個評論,又是靜待審覈,而後是Gradle的配置,GPG簽名等,更多具體詳細內容能夠參考:Android庫發佈至MavenCentral流程詳解
建立配置dsl,先定義dsl結構,定義裏面的屬性,而後在plugin apply方法中添加如下:
TestExtension extension = project.getExtensions().create("testExt", TestExtension)
project.extensions.add("testExt", TestExtension)
project.task("TestTask") {
doLast {
//2.獲取外界配置的 TestExtension
TestExtension extension = project.testExt
//3.輸出插件擴展屬性
println ">>>>>>" + extension.message
}
}
testExt {
//給插件擴展的屬性賦值
message "helloworld"
}
複製代碼
沒有啥idea,強行寫個沒啥用的插件沒啥意思,恰好羣裏有人談到了打包插件:walle,直接看人家插件是怎麼實現的~
比起Android自帶打包要快上許多,在美團技術團隊的博客上有介紹這個工具的大概實現原理:新一代開源Android渠道包生成工具Walle,簡單說說,等下慢慢跟一波源碼~
先是v2簽名先後的APK包差別:
多了個 APK Signing Block
區塊,除它以外其它三個區塊都是受保護的,簽名後對這些區塊的修改都逃不過應用簽名方案的檢查。美團的打包插件就是從 APK Signing Block
區塊入手的,區塊2格式描述以下:
而後就是從 ID-value
入手的,v2簽名信息是以 ID(0x7109871a)
的ID-value來保存在這個區塊中,Android系統對於其它的ID-value選擇忽略,打包插件就是定義了自定義的ID-value把渠道信息寫入到這個區域,App運行時讀取渠道信息,再去完成特定渠道初始化。整套插件主要由四個部分組成:
行吧,大概原理就瞭解到這裏,接着跟一波插件具體的實現源碼,直接定位到插件配置文件:
打開看下接口實現類是哪一個:
定位到 GradlePlugin.groovy
,邏輯不算複雜:
定位到 Extension.groovy
,就是DSL傳遞的參數:
對應文檔這裏:
看完 applyExtension()
接着看看 applyTask()
:
跟下 ChannelMaker
Task類~
集成 DefaultTask
, @TaskAction
註解標識Task自己要執行的方法:
而後判斷
根據下述幾種狀況調用對應生成渠道APK的方法:
上述方法各自有不一樣的處理,但最終調用的都是:generateChannelApk
:
嘔吼,接着看看ChannelWriter是怎麼寫入渠道信息的~
跟下 put()
方法,最後調用的都是:
跟下 putRaw()
方法:
定位下 APK_CHANNEL_BLOCK_ID
:
哈,這個插件把渠道信息寫到 APK Signing Block
裏的 ID,跟下 PayloadWriter.put()
。
一步步跟,跟到putAll,看上面的代碼彷佛很複雜的樣子?其實否則先是 ApkSigningBlockHandler
回調接口,定義了一個handle方法:
傳入了一個 originIdValues
,其實就是apk自己帶的ID-value,而後遍歷新的 idValues
,寫入其中,最後經過 addPayload()
將數據塞到 ApkSigningBlock
實例中返回。
接着是v3簽名的一些處理,而後把渠道信息寫到apk裏,這裏就真的是 技術活,太強了!!!
byte級別精細化的文件操做,這功底...個人確是個假安卓,跟到 ApkSigningBlock → writeApkSigningBlock()
就是對應上表,把區塊2的內容寫回apk中:
把渠道包信息打入到apk的大概流程就這樣,任務的執行時機也在 assemble
後。
寫弄懂了,接着看下讀,官方文檔中寫道:
APP還要另外依賴這個aar,在運行時讀取對應的渠道信息
跟下 WalleChannelReader
,在項目的 library
目錄下:
傳入comtext,獲取apk的路徑,接着傳入 ChannelReader.get()
:
跟下getMap():
跟下 PayloadReader.getString()
getString() → get() → getAll()
RandomAccessFile.getChannel() 得到文件通道對象,而後傳入 ApkUtil.findApkSigningBlock()
就是找到 APK Signing Block
區塊,返回ByteBuffer實例,而後 ApkUtil.findIdValues(apkSigningBlock2)
獲取Id-Values 們,此時再回到 ChannelReader → get()
處,就懂了吧。
這是調用 getChannel()
的實現,若是根據 key
獲取則是走 getChannelInfoMap()
,流程比較類似,就再也不復述了。
以上就是此插件實現的完整講解,固然核心難點Byte級別的文件操做,後面解完apk構建過程再去研究研究~
參考文獻:
本文正在參與「掘金小冊免費學啦!」活動, 點擊查看活動詳情