自定義 Gradle 插件的本質就是把邏輯獨立的代碼進行抽取和封裝,以便於咱們更高效地經過插件依賴這一方式進行功能複用。html
而在 Android 下的 gradle 插件共分爲 兩大類,以下所示:java
腳本插件
:
同普通的 gradle 腳本編寫形式同樣,經過 apply from: 'JsonChao.gradle' 引用。
對象插件
:
經過插件全路徑類名或 id 引用,它主要有
三種編寫形式,以下所示:
下面👇,咱們就先來看看如何編寫一個腳本插件。android
同普通的 gradle 腳本編寫形式同樣,咱們既能夠寫在 build.gradle 裏面,也能夠本身新建一個 gradle 腳本文件進行編寫。git
class PluginDemo implements Plugin<Project> {
@Override
void apply(Project target) {
println 'Hello author!'
}
}
複製代碼
而後,在須要使用的 gradle 腳本中經過 apply plugin: pluginName
的方式便可引用對應的插件。github
apply plugin: PluginDemo
複製代碼
在完成幾個自定義 Gradle 插件以後,我發現 在 buildSrc 目錄下編寫插件的方式是開發效率最高的,首先,buildSrc 是默認的插件目錄,其次,在 buildSrc 目錄下與獨立工程的插件工程同樣,也可以發佈插件,這裏僅僅只需對某些配置作一些調整便可,下面,咱們就來看看如何來建立一個自定義 Gradle 插件。web
首先須要瞭解的是,buildSrc 目錄是 gradle 默認的構建目錄之一,該目錄下的代碼會在構建時自動地進行編譯打包,而後它會被添加到 buildScript 中的 classpath 下,因此不須要任何額外的配置,就能夠直接被其餘模塊中的 gradle 腳本引用。此外,關於 buildSrc,咱們還須要注意如下 兩點:json
1)、新建一個 module,並將其命名爲 buildSrc。這樣,Gradle 默認會將其識別會工程的插件目錄。c#
2)、src 目錄下刪除僅保留一個空的 main 目錄,並在 main 目錄下新建 1 個 groovy 目錄與 1 個 resources 目錄。api
3)、將 buildSrc 中的 build.gradle 中的全部配置刪去,並配置 groovy、resources 爲源碼目錄與相關依賴便可。配置代碼以下所示:緩存
apply plugin: 'groovy'
repositories {
google()
mavenCentral()
jcenter()
}
dependencies {
// Groovy DSL
implementation localGroovy()
// Gradle DSL
implementation gradleApi()
// Android DSL
implementation 'com.android.tools.build:gradle:3.6.2'
// ASM V7.1
implementation group: 'org.ow2.asm', name: 'asm', version: '7.1'
implementation group: 'org.ow2.asm', name: 'asm-commons', version: '7.1'
}
sourceSets {
main {
groovy {
srcDir 'src/main/groovy'
}
resources {
srcDir 'src/main/resources'
}
}
}
複製代碼
1)、首先,在個人 main 目錄下建立一個遞歸文件夾 "com.json.chao.study",裏面直接新建一個名爲 CustomGradlePlugin 的普通文件。而後,在文件中寫入 'class CustomGradlePlugin' ,這時 CustomGradlePlugin 會被自動識別爲類,接着將其實現 Plugin 接口,其中的 apply 方法就是插件被引入時要執行的方法,這樣,自定義插件類就基本完成了,CustomGradlePlugin 類的代碼以下所示:
/**
* 自定義插件
*/
class CustomGradlePlugin implements Plugin<Project> {
/**
* 插件被引入時要執行的方法
* @param project 引入當前插件的 project
*/
@Override
void apply(Project project) {
println "Hello plugin..." + project.name
}
}
複製代碼
2)、接着,在 resources 目錄下建立一個 META-INF.gradle-plugins 的遞歸目錄,裏面新建一個 "com.json.chao.study.properties" 文件,其中 '.properties' 前面的名字即爲 自定義插件的名字,在該文件中,咱們須要標識該插件對應的插件實現類,代碼以下所示:
implementation-class=com.json.chao.study.CustomGradlePlugin
複製代碼
這樣,一個最簡單的自定義插件就完成了。接着,咱們直接在 app moudle 下的 build.gradle 文件中使用 'apply plugin: 'com.json.chao.study' 引入咱們定義好的插件而後同步工程便可看到以下輸出:
...
> Configure project :app
Hello plugin...app
...
複製代碼
能夠看到,經過 id 引用的方式,咱們能夠隱藏類名等細節,使得插件的引用變得更加容易。
在 深度探索 Gradle 自動化構建技術(3、Gradle 核心解密) 一文中咱們講解了如何建立一個版本信息管理的 task,這裏咱們就能夠直接將它接入到 gradle 的構建流程之中。
爲了能讓 App 傳入相關的版本信息和生成的版本信息文件路徑,咱們須要一個用於配置版本信息的 Extension,其實質就是一個實體類,以下所示:
/*
* Description: 負責 Release 版本管理的擴展屬性區域
*
* @author quchao
*/
class ReleaseInfoExtension {
String versionName;
String versionCode;
String versionInfo;
String fileName;
}
複製代碼
而後,在咱們的 CustomGradlePlugin 的 apply 方法中加入下面代碼去建立用於設置版本信息的擴展屬性,以下所示:
// 建立用於設置版本信息的擴展屬性
project.extensions.create("releaseInfo", ReleaseInfoExtension.class)
複製代碼
在 project.extensions.create 方法的內部其實質是 經過 project.extensions.create() 方法來獲取在 releaseInfo 閉包中定義的內容並經過反射將閉包的內容轉換成一個 ReleaseInfoExtension 對象。
最後,咱們就能夠在 app moudle 的 build.gradle 腳本中使用 releaseInfo 去配置擴展屬性,代碼以下所示:
releaseInfo {
versionCode = "1"
versionName = "1.0.0"
versionInfo = "第一個版本~"
fileName = "releases.xml"
}
複製代碼
使用自定義擴展屬性 Extension 僅僅是爲了讓使用插件者有配置插件的能力。而插件還得藉助自定義 Task 來實現相應的功能,這裏咱們須要建立一個更新版本信息的 Task,咱們將其命名爲 ReleaseInfoTask,其具體實現代碼以下所示:
/**
* 更新版本信息的 Task
*/
class ReleaseInfoTask extends DefaultTask {
ReleaseInfoTask() {
// 一、在構造器中配置了該 Task 對應的 Task group,即 Task 組,併爲其添加上了對應的描述信息。
group = 'version_manager'
description = 'release info update'
}
// 二、在 gradle 執行階段執行
@TaskAction
void doAction() {
updateVersionInfo();
}
private void updateVersionInfo() {
// 三、從 realeaseInfo Extension 屬性中獲取相應的版本信息
def versionCodeMsg = project.extensions.releaseInfo.versionCode;
def versionNameMsg = project.extensions.releaseInfo.versionName;
def versionInfoMsg = project.extensions.releaseInfo.versionInfo;
def fileName = project.extensions.releaseInfo.fileName;
def file = project.file(fileName)
// 四、將實體對象寫入到 xml 文件中
def sw = new StringWriter()
def xmlBuilder = new MarkupBuilder(sw)
if (file.text != null && file.text.size() <= 0) {
//沒有內容
xmlBuilder.releases {
release {
versionCode(versionCodeMsg)
versionName(versionNameMsg)
versionInfo(versionInfoMsg)
}
}
//直接寫入
file.withWriter { writer -> writer.append(sw.toString())
}
} else {
//已有其它版本內容
xmlBuilder.release {
versionCode(versionCodeMsg)
versionName(versionNameMsg)
versionInfo(versionInfoMsg)
}
//插入到最後一行前面
def lines = file.readLines()
def lengths = lines.size() - 1
file.withWriter { writer ->
lines.eachWithIndex { line, index ->
if (index != lengths) {
writer.append(line + '\r\n')
} else if (index == lengths) {
writer.append('\r\r\n' + sw.toString() + '\r\n')
writer.append(lines.get(tlengths))
}
}
}
}
}
}
複製代碼
首先,在註釋1處,咱們 在構造器中配置了該 Task 對應的 Task group,即 Task 組,併爲其添加上了對應的描述信息。接着,在註釋2處,咱們 使用了 @TaskAction 註解標註了 doAction 方法,這樣它就會在 gradle 執行階段執行。在註釋3處,咱們 使用了 project.extensions.releaseInfo.xxx 一系列 API 從 realeaseInfo Extension 屬性中了獲取相應的版本信息。最後,註釋4處,就是用來 實現該 task 的核心功能,即將實體對象寫入到 xml 文件中。
能夠看到,通常的插件 task 都會遵循前三個步驟,最後一個步驟就是用來實現插件的核心功能。
固然,最後別忘了在咱們的 CustomGradlePlugin 的 apply 方法中加入下面代碼去建立 ReleaseInfoTask 實例,代碼以下所示:
// 建立用於更新版本信息的 task
project.tasks.create("releaseInfoTask", ReleaseInfoTask.class)
複製代碼
要理解 Variants 的做用,就必須先了解 flavor、dimension 與 variant 這三者之間的關係。在 android gradle plugin V3.x 以後,每一個 flavor 必須對應一個 dimension,能夠理解爲 flavor 的分組,而後不一樣 dimension 裏的 flavor 會組合成一個 variant。示例代碼以下所示:
flavorDimensions "size", "color"
productFlavors {
JsonChao {
dimension "size"
}
small {
dimension "size"
}
blue {
dimension "color"
}
red {
dimension "color"
}
}
複製代碼
在 Android 對 Gradle 插件的擴展支持之中,其中最經常使用的即是 利用變體(Variants)來對構建過程當中的各個默認的 task 進行 hook。關於 Variants 共有 三種類型,以下所示:
applicationVariants
:
只適用於 app plugin。
libraryVariants
:
只適用於 library plugin。
testVariants
:
在 app plugin 與 libarary plugin 中都適用。
爲了講解 applicationVariants 的做用,咱們須要先在 app moudle 的 build.gradle 文件中配置幾個 flavor,代碼以下所示:
productFlavors {
douyin {}
weixin {}
google {}
}
複製代碼
而後,咱們能夠 使用 applicationVariants.all 在配置階段以後去獲取全部 variant 的 name 與 baseName。代碼以下所示:
this.afterEvaluate {
this.android.applicationVariants.all { variant ->
def name = variant.name
def baseName = variant.baseName
println "name: $name, baseName: $baseName"
}
}
複製代碼
最後,執行 gradle clean task
,其輸出信息以下所示:
> Configure project :app
name: douyinDebug, baseName: douyin-debug
name: douyinRelease, baseName: douyin-release
name: weixinDebug, baseName: weixin-debug
name: weixinRelease, baseName: weixin-release
name: googleDebug, baseName: google-debug
name: googleRelease, baseName: google-release
複製代碼
能夠看到,name 與 baseName 的區別:baiduDebug 與 baidu-debug 。
this.afterEvaluate {
this.android.applicationVariants.all { variant ->
variant.outputs.each {
// 因爲咱們當前的變體是 application 類型的,因此
// 這個 output 就是咱們 APK 文件的輸出路徑,咱們
// 能夠經過重命名這個文件來修改咱們最終輸出的 APK 文件
outputFileName = "app-${variant.baseName}-${variant.versionName}.apk"
println outputFileName
}
}
}
複製代碼
執行 gradle clean task
,其輸出信息以下所示:
> Configure project :app
app-debug-1.0.apk
app-release-1.0.apk
複製代碼
咱們能夠在 android.applicationVariants.all
的閉包中經過 variant.task
來獲取相應的 Task。代碼以下所示:
this.afterEvaluate {
this.android.applicationVariants.all { variant ->
def task = variant.checkManifest
println task.name
}
}
複製代碼
而後,執行 gradle clean task
,其輸出信息以下所示:
checkDebugManifest
checkReleaseManifest
複製代碼
既然能夠獲取到變體中的 Task,咱們就能夠根據不一樣的 Task 類型來作特殊處理。例如,咱們能夠利用 variants 去解決插件化開發中的痛點:編寫一個對插件化項目中的各個插件自動更新的腳本,其核心代碼以下所示:
this.afterEvaluate {
this.android.applicationVariants.all { variant ->
// checkManifest 這個 Task 在 Task 容器中
// 靠前的位置,咱們能夠在這裏預先更新插件。
def checkTask = variant.checkManifest
checkTask.doFirst {
def bt = variant.buildType.name
if (bt == 'qa' || bt == 'preview'
|| bt == 'release') {
update_plugin(bt)
}
}
}
}
複製代碼
至於 update_plugin 的實現,主要就是一些插件安全校驗與下載的邏輯,這部分其實跟 Gradle 沒有什麼聯繫,若是有須要,能夠在 Awesome-WanAndroid 項目下查看。
衆所周知,Google 官方在 Android Gradle V1.5.0 版本之後提供了 Transfrom API, 容許第三方 Plugin 在打包成 .dex 文件以前的編譯過程當中操做 .class 文件,咱們須要作的就是實現 Transform 來對 .class 文件遍歷以拿到全部方法,修改完成後再對原文件進行替換便可。
總的來講,Gradle Transform 的功能就是把輸入的 .class 文件轉換爲目標字節碼文件。
下面,咱們來了解一下 Transform 的兩個基礎概念。
TransformInput 可認爲是全部輸入文件的一個抽象,它主要包括兩個部分,以下所示:
DirectoryInput
集合:
表示以源碼方式參與項目編譯的全部目錄結構與其目錄下的源碼文件。
JarInput 集合
:
表示以 jar 包方式參與項目編譯的全部本地 jar 包和遠程 jar 包。須要注意的是,這個 jar 所指也包括 aar。
表示 Transform 的輸出,利用它咱們能夠 獲取輸出路徑等信息。
// 因爲 buildSrc 的執行時機要早於任何一個 project,所以須要⾃⼰添加倉庫
repositories {
google()
jcenter()
}
dependencies {
// Android DSL
implementation 'com.android.tools.build:gradle:3.6.2'
}
複製代碼
其建立步驟能夠細分爲五步,以下所示:
下面👇,咱們來分別來進行詳細講解。
每個 Transform 都有一個與之對應的 Transform task,這裏即是返回的 task name。它會出如今 app/build/intermediates/transforms 目錄下。其代碼以下所示:
/**
* 每個 Transform 都有一個與之對應的 Transform task,
* 這裏即是返回的 task name。它會出如今
* app/build/intermediates/transforms 目錄下
*
* @return Transform Name
*/
@Override
String getName() {
return "MyCustomTransform"
}
複製代碼
getInputTypes 方法用於肯定咱們須要對哪些類型的結果進行轉換:如字節碼、資源⽂件等等。目前 ContentType 有六種枚舉類型,一般咱們使用比較頻繁的有前兩種,以下所示:
CONTENT_CLASS
:
表示須要處理 java 的 class 文件。
CONTENT_JARS
:
表示須要處理 java 的 class 與 資源文件。
CONTENT_RESOURCES
:
表示須要處理 java 的資源文件。
CONTENT_NATIVE_LIBS
:
表示須要處理 native 庫的代碼。
CONTENT_DEX
:
表示須要處理 DEX 文件。
CONTENT_DEX_WITH_RESOURCES
:
表示須要處理 DEX 與 java 的資源文件。
由於咱們須要修改的是字節碼,因此直接返回 TransformManager.CONTENT_CLASS
便可,代碼以下所示:
/**
* 須要處理的數據類型,目前 ContentType
* 有六種枚舉類型,一般咱們使用比較頻繁的有前兩種:
* 一、CONTENT_CLASS:表示須要處理 java 的 class 文件。
* 二、CONTENT_JARS:表示須要處理 java 的 class 與 資源文件。
* 三、CONTENT_RESOURCES:表示須要處理 java 的資源文件。
* 四、CONTENT_NATIVE_LIBS:表示須要處理 native 庫的代碼。
* 五、CONTENT_DEX:表示須要處理 DEX 文件。
* 六、CONTENT_DEX_WITH_RESOURCES:表示須要處理 DEX 與 java 的資源文件。
*
* @return
*/
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
// 用於肯定咱們須要對哪些類型的結果進行轉換:如字節碼、資源⽂件等等。
// return TransformManager.RESOURCES
return TransformManager.CONTENT_CLASS
}
複製代碼
getScopes 方法則是用於肯定插件的適用範圍:目前 Scope 有 五種基本類型,以下所示:
PROJECT
:只有項目內容。
SUB_PROJECTS
:只有子項目。
EXTERNAL_LIBRARIES
:只有外部庫,
TESTED_CODE
:由當前變體(包括依賴項)所測試的代碼。
PROVIDED_ONLY
:只提供本地或遠程依賴項。
此外,還有一些複合類型,它們是都是由這五種基本類型組成,以實現靈活肯定自定義插件的範圍,這裏一般是指定整個 project,也能夠指定其它範圍,其代碼以下所示:
/**
* 表示 Transform 要操做的內容範圍,目前 Scope 有五種基本類型:
* 一、PROJECT 只有項目內容
* 二、SUB_PROJECTS 只有子項目
* 三、EXTERNAL_LIBRARIES 只有外部庫
* 四、TESTED_CODE 由當前變體(包括依賴項)所測試的代碼
* 五、PROVIDED_ONLY 只提供本地或遠程依賴項
* SCOPE_FULL_PROJECT 是一個 Scope 集合,包含 Scope.PROJECT,
Scope.SUB_PROJECTS, Scope.EXTERNAL_LIBRARIES 這三項,即當前 Transform
的做用域包括當前項目、子項目以及外部的依賴庫
*
* @return
*/
@Override
Set<? super QualifiedContent.Scope> getScopes() {
// 適用範圍:一般是指定整個 project,也能夠指定其它範圍
return TransformManager.SCOPE_FULL_PROJECT
}
複製代碼
isIncremental 方法用於肯定是否支持增量更新,若是返回 true,TransformInput 會包含一份修改的文件列表,若是返回 false,則會進行全量編譯,而且會刪除上一次的輸出內容。
@Override
boolean isIncremental() {
// 是否支持增量更新
// 若是返回 true,TransformInput 會包含一份修改的文件列表
// 若是返回 false,會進行全量編譯,刪除上一次的輸出內容
return false
}
複製代碼
在 transform 方法中,就是用來給咱們進行具體的轉換過程的。其實現代碼以下所示:
/**
* 進行具體的轉換過程
*
* @param transformInvocation
*/
@Override
void transform(TransformInvocation transformInvocation) throws
TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
println '--------------- MyTransform visit start --------------- '
def startTime = System.currentTimeMillis()
def inputs = transformInvocation.inputs
def outputProvider = transformInvocation.outputProvider
// 一、刪除以前的輸出
if (outputProvider != null)
outputProvider.deleteAll()
// Transform 的 inputs 有兩種類型,一種是目錄,一種是 jar
包,要分開遍歷
inputs.each { TransformInput input ->
// 二、遍歷 directoryInputs(本地 project 編譯成的多個 class
⽂件存放的目錄)
input.directoryInputs.each { DirectoryInput directoryInput ->
handleDirectory(directoryInput, outputProvider)
}
// 三、遍歷 jarInputs(各個依賴所編譯成的 jar 文件)
input.jarInputs.each { JarInput jarInput ->
handleJar(jarInput, outputProvider)
}
}
def cost = (System.currentTimeMillis() - startTime) / 1000
println '--------------- MyTransform visit end --------------- '
println "MyTransform cost : $cost s"
}
複製代碼
這裏咱們主要是作了三步處理,以下所示:
在 handleDirectory 與 handleJar 方法中則是進行了相應的 文件處理 && ASM 字節碼修改。這裏我直接放出 Transform 的通用模板代碼,代碼以下所示:
class MyTransform extends Transform {
/**
* 每個 Transform 都有一個與之對應的 Transform task,
* 這裏即是返回的 task name。它會出如今 app/build/intermediates/transforms 目錄下
*
* @return Transform Name
*/
@Override
String getName() {
return "MyCustomTransform"
}
/**
* 須要處理的數據類型,目前 ContentType 有六種枚舉類型,一般咱們使用比較頻繁的有前兩種:
* 一、CONTENT_CLASS:表示須要處理 java 的 class 文件。
* 二、CONTENT_JARS:表示須要處理 java 的 class 與 資源文件。
* 三、CONTENT_RESOURCES:表示須要處理 java 的資源文件。
* 四、CONTENT_NATIVE_LIBS:表示須要處理 native 庫的代碼。
* 五、CONTENT_DEX:表示須要處理 DEX 文件。
* 六、CONTENT_DEX_WITH_RESOURCES:表示須要處理 DEX 與 java 的資源文件。
*
* @return
*/
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
// 用於肯定咱們須要對哪些類型的結果進行轉換:如字節碼、資源⽂件等等。
// return TransformManager.RESOURCES
return TransformManager.CONTENT_CLASS
}
/**
* 表示 Transform 要操做的內容範圍,目前 Scope 有五種基本類型:
* 一、PROJECT 只有項目內容
* 二、SUB_PROJECTS 只有子項目
* 三、EXTERNAL_LIBRARIES 只有外部庫
* 四、TESTED_CODE 由當前變體(包括依賴項)所測試的代碼
* 五、PROVIDED_ONLY 只提供本地或遠程依賴項
* SCOPE_FULL_PROJECT 是一個 Scope 集合,包含 Scope.PROJECT, Scope.SUB_PROJECTS, Scope.EXTERNAL_LIBRARIES 這三項,即當前 Transform 的做用域包括當前項目、子項目以及外部的依賴庫
*
* @return
*/
@Override
Set<? super QualifiedContent.Scope> getScopes() {
// 適用範圍:一般是指定整個 project,也能夠指定其它範圍
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
// 是否支持增量更新
// 若是返回 true,TransformInput 會包含一份修改的文件列表
// 若是返回 false,會進行全量編譯,刪除上一次的輸出內容
return false
}
/**
* 進行具體的轉換過程
*
* @param transformInvocation
*/
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
println '--------------- MyTransform visit start --------------- '
def startTime = System.currentTimeMillis()
def inputs = transformInvocation.inputs
def outputProvider = transformInvocation.outputProvider
// 刪除以前的輸出
if (outputProvider != null)
outputProvider.deleteAll()
// Transform 的 inputs 有兩種類型,一種是目錄,一種是 jar 包,要分開遍歷
inputs.each { TransformInput input ->
// 遍歷 directoryInputs(本地 project 編譯成的多個 class ⽂件存放的目錄)
input.directoryInputs.each { DirectoryInput directoryInput ->
handleDirectory(directoryInput, outputProvider)
}
// 遍歷 jarInputs(各個依賴所編譯成的 jar 文件)
input.jarInputs.each { JarInput jarInput ->
handleJar(jarInput, outputProvider)
}
}
def cost = (System.currentTimeMillis() - startTime) / 1000
println '--------------- MyTransform visit end --------------- '
println "MyTransform cost : $cost s"
}
static void handleJar(JarInput jarInput, TransformOutputProvider outputProvider) {
if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
// 截取文件路徑的 md5 值重命名輸出文件,避免出現同名而覆蓋的狀況出現
def jarName = jarInput.name
def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
JarFile jarFile = new JarFile(jarInput.file)
Enumeration enumeration = jarFile.entries()
File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
// 避免上次的緩存被重複插入
if (tmpFile.exists()) {
tmpFile.delete()
}
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement()
String entryName = jarEntry.getName()
ZipEntry zipEntry = new ZipEntry(entryName)
InputStream inputStream = jarFile.getInputStream(jarEntry)
if (checkClassFile(entryName)) {
// 使用 ASM 對 class 文件進行操控
println '----------- deal with "jar" class file <' + entryName + '> -----------'
jarOutputStream.putNextEntry(zipEntry)
ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
ClassWriter classWriter = new ClassWriter(classReader, org.objectweb.asm.ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new MyCustomClassVisitor(classWriter)
classReader.accept(cv, EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
jarOutputStream.write(code)
} else {
jarOutputStream.putNextEntry(zipEntry)
jarOutputStream.write(IOUtils.toByteArray(inputStream))
}
jarOutputStream.closeEntry()
}
jarOutputStream.close()
jarFile.close()
// 生成輸出路徑 dest:./app/build/intermediates/transforms/xxxTransform/...
def dest = outputProvider.getContentLocation(jarName + md5Name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
// 將 input 的目錄複製到 output 指定目錄
FileUtils.copyFile(tmpFile, dest)
tmpFile.delete()
}
}
static void handleDirectory(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
// 在增量模式下能夠經過 directoryInput.changedFiles 方法獲取修改的文件
// directoryInput.changedFiles
if (directoryInput.file.size() == 0)
return
if (directoryInput.file.isDirectory()) {
/**遍歷以某一擴展名結尾的文件*/
directoryInput.file.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) {
File classFile ->
def name = classFile.name
if (checkClassFile(name)) {
println '----------- deal with "class" file <' + name + '> -----------'
def classReader = new ClassReader(classFile.bytes)
def classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
def classVisitor = new MyCustomClassVisitor(classWriter)
classReader.accept(classVisitor, EXPAND_FRAMES)
byte[] codeBytes = classWriter.toByteArray()
FileOutputStream fileOutputStream = new FileOutputStream(
classFile.parentFile.absolutePath + File.separator + name
)
fileOutputStream.write(codeBytes)
fileOutputStream.close()
}
}
}
/// 獲取 output 目錄 dest:./app/build/intermediates/transforms/hencoderTransform/
def destFile = outputProvider.getContentLocation(
directoryInput.name,
directoryInput.contentTypes,
directoryInput.scopes,
Format.DIRECTORY
)
// 將 input 的目錄複製到 output 指定目錄
FileUtils.copyDirectory(directoryInput.file, destFile)
}
/**
* 檢查 class 文件是否須要處理
*
* @param fileName
* @return class 文件是否須要處理
*/
static boolean checkClassFile(String name) {
// 只處理須要的 class 文件
return (name.endsWith(".class") && !name.startsWith("R\$")
&& "R.class" != name && "BuildConfig.class" != name
&& "android/support/v4/app/FragmentActivity.class" == name)
}
複製代碼
編寫完 Transform 的代碼以後,咱們就能夠 在 CustomGradlePlugin 的 apply 方法中加入下面代碼去註冊 MyTransform 實例,代碼以下所示:
// 註冊咱們自定義的 Transform
def appExtension = project.extensions.findByType(AppExtension.class)
appExtension.registerTransform(new MyTransform());
複製代碼
上面的自定義 Transform 的代碼就是一個標準的 Transorm + ASM 修改字節碼的模板代碼,在使用時,咱們只須要編寫咱們本身的 MyClassVisitor 類去修改相應的字節碼文件便可,關於 ASM 的使用能夠參考我前面寫的 深刻探索編譯插樁技術(4、ASM 探祕) 一文。
咱們能夠自定義一個 Gradle Plugin,而後註冊一個 Transform 對象,在 tranform 方法裏,能夠分別遍歷目錄和 jar 包,而後咱們就能夠遍歷當前應用程序的全部 .class 文件,而後在利用 ASM 框架的 Core API 去加載相應的 .class 文件,並解析,就能夠找到知足特定條件的 .class 文件和相關方法,最後去修改相應的方法以實現動態插入相應的字節碼。
發佈插件能夠分爲 兩種形式,以下所示:
下面,咱們就來使用 mavenDeployer
插件來將插件分別發佈在本地倉庫和遠程倉庫。
引入 maven 插件以後,咱們 在 uploadArchives 加入想要上傳的倉庫地址與相關配置便可,這樣 Gradle 在執行 uploadArchives 時將生成和上傳 pom.xml 文件,將插件上傳至本地倉庫的示例代碼以下所示:
apply plugin: 'maven'
uploadArchives {
repositories {
mavenDeployer {
// 上傳到當前項目根目錄下的本地 repo 目錄中
repository(url: uri('../repo'))
pom.groupId = 'com.json.chao.study'
pom.artifactId = 'custom-gradle-plugin'
pom.version = '1.0.0'
}
}
}
複製代碼
能夠看到,這裏咱們將本地倉庫路徑指定爲了根目錄下的 repo 文件夾。此外,咱們須要配置插件中的一些屬性信息,一般包含以下三種:
groupId
:
組織/公司名稱。
artifactId
:
項目/模塊名稱。
version
:
項目/模塊的當前版本號。
apply plugin: 'maven'
uploadArchives {
configuration = configurations.archives
repositories {
mavenDeployer {
repository(url: MAVEN_REPO_RELEASE_URL) {
authentication(userName: "JsonChao", password: "123456")
}
pom.groupId = 'com.json.chao.study'
pom.artifactId = 'custom-gradle-plugin'
pom.version = '1.0.0'
}
}
}
複製代碼
不一樣於發佈插件到本地倉庫的方式,發佈插件到遠程倉庫僅僅是將 repository 中的 url 替換爲 遠程 maven 倉庫的 url,並將須要認證的 userName 與 password 進行配置便可。
將插件配置好了以後,咱們就能夠經過 ./gradlew uploadArchivers
來執行這個 task,實現將插件發佈到本地/遠程倉庫。
最後,咱們只須要輸入插件的 Name 便可,咱們這裏的插件名字是 plugin-release。
./gradlew --no-daemon -Dorg.gradle.debug=true :app:assembleRelease
複製代碼
在本文中,咱們一塊兒學習瞭如何自定義一個 Gradle 插件,若是你還沒建立一個屬於本身的自定義 Gradle 插件,我強烈建議你去嘗試一下。固然,僅僅會製造一個自定義 Gradle 插件還遠遠不夠,在下一篇文章中,咱們會來全方位地深刻剖析 Gradle 插件架構體系中涉及的核心原理,盡請期待~
歡迎關注個人微信:
bcce5360
因爲微信羣已超過 200 人,麻煩你們想進微信羣的朋友們,加我微信拉你進羣。
2千人QQ羣,Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎你們加入~