補齊Android技能樹 - 玩轉Gradle插件 | 小冊免費學

在上一節《補齊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

0x一、Gradle插件分類

1. 構建腳本

嚴格來講不上插件,就是把構建腳本代碼寫到一個單獨的文件中,複製粘貼到項目目錄下引用。示例以下: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 執行任務結果輸出以下:閉包


2. buildSrc項目

執行Gradle時會將根目錄下的buildSrc目錄做爲插件源碼目錄進行編譯,並將結果加入到構建腳本的classpath中。app

這種插件不須要plugins{}引入,通常適合沒有複用性的插件或者新插件開發調試用,還有個缺點:沒法使用屬性配置DSL,需經過 configure<...> {...} 配置插件屬性。


3. 獨立項目

將項目打成jar包,可在多個項目間複用,通常的插件開發都是指的這一類~


0x二、Gradle Plugin Demo 初體驗

咱們上面經過命令行建立了一個Gradle插件項目,接着來康一康都有哪些核心要素( 順帶吐槽下網上一堆說得不清不楚的文章...)

1. 項目組成

插件實現類: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就能夠了,無需另外再配置一遍!


2. 插件本地發佈

使用插件可使用上面的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 '任務執行完畢' }
}
複製代碼

運行結果以下:


3. 插件遠程發佈

① 廢棄的JCenter

說到插件遠程發佈,網上十有八九的教程都是傳到 JFlog Bintray,打開官網倒是大紅提示:

其實早在今年的2.3,官方就發佈了一則通告:

包括 GoCenter、Bintray、JCenter 在內的多項軟件包管理和分發服務都將中止運營。

自3.31後就不在接受任何新的提交,在2022.2.1前,你仍是能夠正常拉取2021.3.31前提交的庫。

不能提交的話就只能找找JCenter的替代品咯,由於這裏編寫的是Gradle插件,能夠試下提交到Gradle的遠程倉庫。


② Gradle Plugin倉庫

完整流程介紹可參見官方文檔: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,只是演示下流程~


③ JitPack倉庫

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倉庫

上傳庫到MavenCentral前須要註冊登陸:Sonatype,進入網頁後點擊Sign up進入註冊頁面註冊:

擁有Sonatype帳號後,點開 管理後臺,Log In下,會彈下述錯誤提示框:

須要申請一波Sonatype上傳權限,回到 issues.sonatype.org/ 頁,點擊新建,填寫項目信息:

提交完等審覈吧,通常會讓你證實域名真的是你本身的:

第一個種解決方式最簡單,域名DNS坐下解析,添加一個TXT類型的記錄便可,如:

接着在官網那裏回覆下他這個評論,又是靜待審覈,而後是Gradle的配置,GPG簽名等,更多具體詳細內容能夠參考:Android庫發佈至MavenCentral流程詳解

0x三、經常使用API補漏

建立配置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"
}
複製代碼

0x四、插件源碼探索——美團渠道包生成插件Walle

沒有啥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運行時讀取渠道信息,再去完成特定渠道初始化。整套插件主要由四個部分組成:

  • ① 用於寫入ID-value信息的Java類庫;
  • ② Gradle構建插件用來和Android的打包流程進行結合 ;
  • ③ 用於讀取ID-value信息的Java類庫;
  • ④ 用於供 com.android.application 使用的讀取渠道信息的AAR;

行吧,大概原理就瞭解到這裏,接着跟一波插件具體的實現源碼,直接定位到插件配置文件:

打開看下接口實現類是哪一個:

1. GradlePlugin

定位到 GradlePlugin.groovy,邏輯不算複雜:

定位到 Extension.groovy,就是DSL傳遞的參數:

對應文檔這裏:

看完 applyExtension() 接着看看 applyTask()

跟下 ChannelMaker Task類~

2. ChannelMaker

集成 DefaultTask@TaskAction 註解標識Task自己要執行的方法:

而後判斷

根據下述幾種狀況調用對應生成渠道APK的方法:

  • PROPERTY_CHANNEL_LIST→ channelList.each { generateChannelApk(...) }
  • PROPERTY_CONFIG_FILEgenerateChannelApkByConfigFile(...)
  • PROPERTY_CHANNEL_FILEgenerateChannelApkByChannelFile(...)
  • configFile instanceof FilegenerateChannelApkByConfigFile(...)
  • channelFile instanceof FilegenerateChannelApkByChannelFile(...)
  • variantConfigFileName != null && variantConfigFileName.length() > 0generateChannelApkByConfigFile(...)

上述方法各自有不一樣的處理,但最終調用的都是:generateChannelApk

嘔吼,接着看看ChannelWriter是怎麼寫入渠道信息的~

3. ChannelWriter

跟下 put() 方法,最後調用的都是:

跟下 putRaw() 方法:

定位下 APK_CHANNEL_BLOCK_ID

哈,這個插件把渠道信息寫到 APK Signing Block 裏的 ID,跟下 PayloadWriter.put()

4. PayloadWriter

一步步跟,跟到putAll,看上面的代碼彷佛很複雜的樣子?其實否則先是 ApkSigningBlockHandler 回調接口,定義了一個handle方法:

傳入了一個 originIdValues,其實就是apk自己帶的ID-value,而後遍歷新的 idValues,寫入其中,最後經過 addPayload() 將數據塞到 ApkSigningBlock 實例中返回。

接着是v3簽名的一些處理,而後把渠道信息寫到apk裏,這裏就真的是 技術活,太強了!!!

byte級別精細化的文件操做,這功底...個人確是個假安卓,跟到 ApkSigningBlock → writeApkSigningBlock()

就是對應上表,把區塊2的內容寫回apk中:

把渠道包信息打入到apk的大概流程就這樣,任務的執行時機也在 assemble 後。

寫弄懂了,接着看下讀,官方文檔中寫道:

APP還要另外依賴這個aar,在運行時讀取對應的渠道信息

跟下 WalleChannelReader,在項目的 library 目錄下:

5. WalleChannelReader

傳入comtext,獲取apk的路徑,接着傳入 ChannelReader.get()

6. ChannelReader

跟下getMap():

跟下 PayloadReader.getString()

7. PayloadReader

getString() → get() → getAll()

RandomAccessFile.getChannel() 得到文件通道對象,而後傳入 ApkUtil.findApkSigningBlock()

就是找到 APK Signing Block 區塊,返回ByteBuffer實例,而後 ApkUtil.findIdValues(apkSigningBlock2) 獲取Id-Values 們,此時再回到 ChannelReader → get() 處,就懂了吧。

這是調用 getChannel() 的實現,若是根據 key 獲取則是走 getChannelInfoMap(),流程比較類似,就再也不復述了。

以上就是此插件實現的完整講解,固然核心難點Byte級別的文件操做,後面解完apk構建過程再去研究研究~

image.png


參考文獻

本文正在參與「掘金小冊免費學啦!」活動, 點擊查看活動詳情

相關文章
相關標籤/搜索