自定義Gradle-Plugin 外掛
官方文件給出了詳細的實現步驟,筆者 將參考官方文件做一些基礎介紹,額外增加一個例項:通過自定義外掛修改編譯後的class檔案,本文按照以下三個方面進行講解
- 外掛基礎介紹
- 三種外掛的打包方式
- 例項Demo&Debug除錯
外掛基礎介紹
根據外掛官方文件定義,外掛打包了可重用的構建邏輯,可以適用不同的專案和構建。
Gradle 提供了很多官方外掛,用於支援Java、Groovy等工程的構建和打包。同時也提供了自定義外掛機制,讓每個人都可以通過外掛來實現特定的構建邏輯,並可以把這些邏輯打包起來,分享給其他人。
外掛的原始碼可以是用Groovy、Scale、Java三種語言,筆者對Scale不熟悉,對Groovy也略知一二。Groovy用於實現構建生命週期(如Task的依賴)有關邏輯,Java用於實現核心邏輯,表現為Groovy呼叫Java程式碼
另外,還有很多專案使用Eclipse 或者Maven進行開發構建,用Java實現核心業務程式碼,將有利於實現快速遷移。
三種外掛的實現方式
筆者編寫自定義外掛相關程式碼時,對很多GradlePluginForAndroid
相關api 不熟悉,例如Transform
、TransformOutputProvider
等,沒關係,官方文件gradle-plugin-android-api 將會是你最好的學習教程
Build Script
把外掛寫在build.gradle 檔案中,一般用於簡單的邏輯,只在改build.gradle 檔案中可見,筆者常用來做原型除錯。在我們指定的module build.gradle 中:
/**
* 分別定義Extension1 和 Extension2 類,申明引數傳遞變數
*/
class Extension1 {
String testVariable1 = null
}
class Extension2 {
String testVariable2 = null
}
/**
* 外掛入口類
*/
class TestPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
//利用Extension建立e1 e2 閉包,用於接受外部傳遞的引數值
project.extensions.create('e1', Extension1)
project.extensions.create('e2', Extension2)
//建立readExtension task 執行該task 進行引數值的讀取以及自定義邏輯...
project.task('readExtension') << {
println 'e1 = ' + project['e1'].testVariable1
println 'e2 = ' + project['e2'].testVariable2
}
}
}
/**
* 依賴我們剛剛自定義的TestPlugin,注意 使用e1 {} || e2{} 一定要放在apply plugin:TestPlugin 後面, 因為 app plugin:TestPlugin
* 會執行 Plugin的apply 方法,進而利用Extension 將e1 、e2 和 Extension1 Extension2 繫結,編譯器才不會報錯
*/
apply plugin: TestPlugin
e1 {
testVariable1 = 'testVariable1'
}
e2 {
testVariable2 = 'testVariable2'
}
複製程式碼
相關注釋說明已經在程式碼中簡單說明,如果讀者依然不熟悉或者想了解更多內容,可以在api文件中進行查閱。
然後執行readExtension
task 即可
./gradlew -p moduledir readExtension --stacktrace
複製程式碼
執行結果
buildSrc 專案
將外掛原始碼放在rootProjectDir/buildScr/scr/main/groovy
中,只對該專案中可見,適用於邏輯較為複雜,但又不需要外部可見的外掛,本文不介紹,有興趣可以參考此處
獨立專案
一個獨立的Groovy 和Java專案,可以把這個專案打包成jar檔案包,一個jar檔案包還可以包含多個外掛入口,可以將檔案包釋出到託管平臺上,共其他人使用。
其實,IntelliJIEDA 開發外掛要比Android Studio要方便一點,因為有對應的Groovy module模板,但如果我們瞭解IDEA專案檔案結構,就不會受到這個侷限,無非就是一個build.gradle 構建資料夾scr原始碼資料夾
-
在Android Studio中新建
Java Library
moduleuploader
(moduleName 不重要,根據實際情況定義) -
修改專案資料夾
- 移除java資料夾,因為在這個專案中用不到java程式碼
- 新增Groovy資料夾,主要的程式碼檔案放在這裡
- 新增resource資料夾,存放用於標識gradle外掛的meta-data
-
修改build.gradle 檔案
//removed java plugin apply plugin: 'groovy' apply plugin: 'maven' repositories { mavenCentral() } dependencies { compile gradleApi()//gradle sdk compile localGroovy()//groovy sdk compile fileTree(dir: 'libs', include: ['*.jar']) } uploadArchives { repositories { mavenDeployer { //設定外掛的GAV引數 pom.groupId = 'cn.andaction.plugin' pom.version = '1.0.0' //檔案釋出到下面目錄 repository(url: uri('../repo')) } } } 複製程式碼
-
建立對應檔案
├── build.gradle
├── libs
├── plugin.iml
└── src
└── main
├── groovy
│ └── cn
│ └── andaction
│ └── uploader
│ ├── XXXPlugin.groovy
│ └── YYYY.groovy
└── resources
└── META-INF
└── gradle-plugins
└── uploader.properties
複製程式碼- Groovy資料夾中的類,一定要修改成
.groovy
字尾,IDE才會正常識別 - resource/META-INF/gradle-plugins這個資料夾結構是強制要求的,否則不能識別成外掛
另外,關於uploader.properties ,寫過java的同學應該知道,這是一個java的properties檔案,是
key=value
的格式,這個檔案內容如下implementation-class=cn.andaction.uploader.XXXPlugin.groovy
複製程式碼用於指定外掛入口類,其中apply plugin: '${當前配置檔名}
- Groovy資料夾中的類,一定要修改成
例項Demo
自定義
gradle-plugin
並利用javassist 類庫工具修改指定編譯後的class檔案
筆者參考了通過自定義Gradle外掛修改編譯後的class檔案
預備知識
-
buid.gradle 增加類庫依賴
compile 'com.android.tools.build:gradle:3.0.1' compile group: 'org.javassist', name: 'javassist', version: '3.22.0-GA' 複製程式碼
-
自定義Transform
public class PreDexTransform extends Transform { private Project project /** * 建構函式 我們將Project 儲存下來備用 * @param project */ PreDexTransform(Project project) { this.project = project } .... @Override void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { //transformInvocation.inputs 有兩種型別,一種是目錄,一種是jar包 分開對其進行遍歷 transformInvocation.inputs.each { TransformInput input -> // 對型別為資料夾 的input進行遍歷 :對應的class位元組碼檔案 // 借用JavaSsist 對資料夾的class 位元組碼 進行修改 input.directoryInputs.each { DirectoryInput directoryInput -> TestInject.injectDir(directoryInput.file.absolutePath, 'cn.andaction.plugin') File des = transformInvocation.getOutputProvider().getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY) FileUtils.copyDirectory(directoryInput.file, des) } // 對型別為jar的input進行遍歷 : 對應三方庫等 input.jarInputs.each { JarInput jarInput -> def jarName = jarInput.name def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath()) if (jarName.endsWith('.jar')) { jarName = jarName.substring(0, jarName.length() - 4) // '.jar'.length == 4 } File dest = transformInvocation.getOutputProvider().getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR) // 將輸入內容複製到輸出 FileUtils.copyFile(jarInput.file, dest) } } super.transform(transformInvocation) } @Override void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException { super.transform(context, inputs, referencedInputs, outputProvider, isIncremental) } } 複製程式碼
-
對directoryInputs 資料夾下的class檔案遍歷,找到符合需要的.class 檔案,通過javassit 類庫對位元組碼檔案進行修改
TestInject.groovy
File dir = new File(path) classPool.appendClassPath(path) if (dir.isDirectory()) { dir.eachFileRecurse { File file -> String filePath = file.path // 這裡我們指定修改TestInjectModel.class位元組碼,在建構函式中增加一行i will inject if (filePath.endsWith('.class') && filePath.endsWith('TestInjectModel.class')) { // 判斷當前目錄是否在我們的應用包裡面 int index = filePath.indexOf(packageName.replace('.',File.separator)) if (index != -1) { int end = filePath.length() - 6 // '.class'.length = 6 String className = filePath.substring(index, end) .replace('\\', '.') .replace('/', '.') // 開始修改class檔案 CtClass ctClass = classPool.getCtClass(className) // 拿到CtClass後可以對 class 做修改操作(addField addMethod ..) if (ctClass.isFrozen()) { ctClass.defrost() } CtConstructor[] constructors = ctClass.getDeclaredConstructors() if (null == constructors || constructors.length == 0) { // 手動建立一個建構函式 CtConstructor constructor = new CtConstructor(new CtClass[0], ctClass) constructor.insertBeforeBody(injectStr) //constructor.insertBefore() 會增加super(),且插入的程式碼在super()前面 ctClass.addConstructor(constructor) } else { constructors[0].insertBeforeBody(injectStr) } ctClass.writeFile(path) ctClass.detach() } } } } 複製程式碼
-
釋出外掛程式碼到本地
./gradlew -p moduleDir/ clean build uploadArchives -stacktrace 複製程式碼
-
執行測試
-
build.gradle
repositories { maven { url 'file:your-project-dir/repo/' } google() jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.0.1' classpath 'cn.andaction.plugin:uploader:1.0.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } 複製程式碼
apply plugin: 'uploader' 複製程式碼
-
修改程式碼
- 新增
TestInjectModel.java
,空實現 - app入口類
onCreate
方法呼叫new TestInjectModle()
- 新增
-
執行
make project
-
-
外掛除錯
參考Android Studio 除錯Gradle-plugin
注意,在修改外掛原始碼後,需要重新執行uploadArchives
釋出外掛程式碼,新增/修改的程式碼斷點才能起作用