Android Studio 自定义Gradle Plugin
一、簡介
之前公司的一個項目需要用到Gradle插件來修改編譯后的class文件,今天有時間就拿出來整理一下,學習一下Gradle插件的編寫還是一件十分有意義的事。
二、Gradle插件類型
-
一種是直接在項目中的gradle文件里編寫,這種方式的缺點是無法復用插件代碼,在其他項目中還得復制一遍代碼(或者說說復制一遍文件)
-
另一種是在獨立的項目里編寫插件,然后發布到中央倉庫,之后直接引用就可以了,優點就是可復用。
今天我們主要來講解下第二種。
三、Gradle插件
Gradle插件是使用Groovy進行開發的,而Groovy其實是可以兼容Java的。Android Studio其實除了開發Android App外,完全可以勝任開發Gradle插件這一工作。
在此之前我們先來了解下 Gradle插件 與 Gradle 的關系:
Gradle插件 版本在項目根目錄下的 build.gradle 中,如下:
dependencies {classpath 'com.android.tools.build:gradle:2.3.0' }而每個Gradle插件版本號又對應有一個或一些 Gradle發行版本(一般是限定一個最低版本),也就是我們常見的類似gradle-3.3-all.zip這種東西,如下:
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip如果這兩個版本對應不上了,那你的工程構建的時候就會報錯。
具體的對應關系如下:
| 1.0.0 - 1.1.3 | 2.2.1 - 2.3 |
| 1.2.0 - 1.3.1 | 2.2.1 - 2.9 |
| 1.5.0 | 2.2.1 - 2.13 |
| 2.0.0 - 2.1.2 | 2.10 - 2.13 |
| 2.1.3 - 2.2.3 | 2.14.1+ |
| 2.3.0+ | 3.3+ |
| 3.0.0+ | 4.1+ |
| 3.1.0+ | 4.4+ |
| 3.2.0 - 3.2.1 | 4.6+ |
| 3.3.0 - 3.3.3 | 4.10.1+ |
| 3.4.0 - 3.4.3 | 5.1.1+ |
| 3.5.0 - 3.5.4 | 5.4.1+ |
| 3.6.0 - 3.6.4 | 5.6.4+ |
| 4.0.0+ | 6.1.1+ |
| 4.1.0+ | 6.5+ |
詳情請見:Android Gradle 插件版本說明
下面來講講具體如何開發。
1、創建插件步驟
第一步:新建一個Android工程
第二步:在該工程中新建一個Android Module項目,類型選擇Android Library
第三步:將Module里的內容刪除,只保留build.gradle文件和src/main目錄,同時移除build.gradle文件里的內容
第四步:建立Gradle插件目錄
由于gradle是基于groovy,因此,我們開發的gradle插件相當于一個groovy項目。所以需要在main目錄下新建groovy目錄,這時候groovy文件夾會被Android識別為groovy源碼目錄。除了在main目錄下新建groovy目錄外,你還要在main目錄下新建resources目錄,同理resources目錄會被自動識別為資源文件夾。在groovy目錄下新建項目包名,就像Java包名那樣。resources目錄下新建文件夾META-INF,META-INF文件夾下新建gradle-plugins文件夾。這樣,就完成了gradle 插件的項目的整體搭建。目前項目的結構是這樣的:
第五步:修改build.gradle文件
內容如下:
apply plugin: 'groovy' apply plugin: 'maven'dependencies{// gradle sdkcompile gradleApi()// groovy sdkcompile localGroovy()compile 'com.android.tools.build:gradle:1.5.0' }repositories{mavenCentral() }第六步:在com.davisplugins包名下通過new -> file ->創建PluginImpl.groovy文件
內容如下:
package com.davispluginsimport com.android.build.gradle.AppExtension import org.gradle.api.Plugin import org.gradle.api.Projectpublic class PluginImpl implements Plugin<Project>{void apply(Project project){System.out.println("========================");System.out.println("hello gradle plugin!");System.out.println("========================");} }第七步:定義插件名稱
在resources/META-INF/gradle-plugins目錄下新建一個properties文件,注意該文件的命名就是你使用插件的名字,這里命名為davis.properties,那么你在其他build.gradle文件中使用自定義的插件時候則需寫成:
apply plugin: 'davis'davis.properties文件內容:
implementation-class=com.davisplugins.PluginImpl注意包名需要替換為你自己的包名。
現在你的目錄結構如下:
2、插件發布
前面我們已經自定義好了插件,接下來就是要打包到Maven庫里面去了,你可以選擇打包到本地,或者是遠程服務器中。
(1)打包到本地Maven倉庫
在我們自定義Module目錄下的build.gradle添加如下代碼:
uploadArchives {repositories {mavenDeployer {pom.groupId = 'com.davisplugins'pom.artifactId = 'davis'pom.version = 1.0// maven本地倉庫的目錄repository(url: uri('../DavisPlugin'))}} }這時候,右側的gradle Toolbar就會在module下多出一個task
點擊uploadArchives這個Task,就會在項目下多出一個DavisPlugin目錄,里面存著這個gradle插件。
(2)發布到遠程Jcenter倉庫
內容更新中…
3、插件的使用
我們來看下,發布到本地maven倉庫的插件如何使用,在項目根目錄下的gradle.build的文件中加入:
buildscript {repositories {// maven插件目錄maven{url uri('DavisPlugin')}jcenter()}dependencies {classpath 'com.android.tools.build:gradle:2.1.0'// 使用自定義插件classpath 'com.davisplugins:davis:1.0'} }allprojects {repositories {jcenter()} }task clean(type: Delete) {delete rootProject.buildDir }app目錄下的build.gradle文件中加入:
apply plugin: 'davis'然后我們就可以使用該插件了,執行一次打包命令看看會發生啥吧!
在打包之前輸出了我們打印的日志信息。
4、最佳實踐
(1)修改編譯后的class文件
我們回到如何修改class文件,首先我們得知道什么時候編譯完成,并且我們要趕在class文件被轉化為dex文件之前去修改。從1.5.0-beta1開始,android的gradle插件引入了com.android.build.api.transform.Transform,可以點擊 http://tools.android.com/tech-docs/new-build-system/transform-api 查看相關內容。Transform每次都是將一個輸入進行處理,然后將處理結果輸出,而輸出的結果將會作為另一個Transform的輸入,過程如下:
注意:輸出地址不是由你任意指定的。而是根據輸入的內容、作用范圍等由TransformOutputProvider生成,比如,你要獲取輸出路徑:
Transform是一個抽象類,我們先自定義一個Transform,如下:
package com.davispluginsimport com.android.build.api.transform.* import com.android.build.gradle.internal.pipeline.TransformManager import org.apache.commons.codec.digest.DigestUtils import org.apache.commons.io.FileUtilspublic class InsertTransform extends Transform {//設置我們自定義的Transform對應的Task名稱@OverrideString getName() {return "DavisPlugin"}//指定輸入的類型,通過這里設定,可以指定我們要處理的文件類型//這樣確保其他類型的文件不會傳入@OverrideSet<QualifiedContent.ContentType> getInputTypes() {return TransformManager.CONTENT_CLASS}//指定Transfrom的作用范圍@OverrideSet<QualifiedContent.Scope> getScopes() {return TransformManager.SCOPE_FULL_PROJECT}@Overrideboolean isIncremental() {return false}@Overridevoid transform(Context context, Collection<TransformInput> inputs,Collection<TransformInput> referencedInputs,TransformOutputProvider outputProvider,boolean isIncremental) throws IOException,TransformException, InterruptedException {} }看到函數transform,我們還沒有具體實現這個函數。這個函數就是具體如何處理輸入和輸出。可以運行一下看看,注意,這里的運行時直接編譯執行我們的apk,而不是像之前那樣直接rebuild,因為rebuild并沒有執行到編譯這一步。由于我們沒有實現transform這個函數,導致沒有輸出!使得整個過程中斷了!最終導致apk運行時找不到MainActivity,所以會報錯。接下來我們去實現以下這個函數,我們啥也不干,就是把輸入內容寫入到作為輸出內容,不做任何處理:
@Overridevoid transform(Context context, Collection<TransformInput> inputs,Collection<TransformInput> referencedInputs,TransformOutputProvider outputProvider,boolean isIncremental) throws IOException, TransformException, InterruptedException {// Transform的inputs有兩種類型,一種是目錄,一種是jar包,要分開遍歷inputs.each { TransformInput input ->//對類型為“文件夾”的input進行遍歷input.directoryInputs.each { DirectoryInput directoryInput ->//文件夾里面包含的是我們手寫的類以及R.class、BuildConfig.class以及R$XXX.class等// 獲取output目錄def dest = outputProvider.getContentLocation(directoryInput.name,directoryInput.contentTypes, directoryInput.scopes,Format.DIRECTORY)//這里執行字節碼的注入,不操作字節碼的話也要將輸入路徑拷貝到輸出路徑FileUtils.copyDirectory(directoryInput.file, dest)}//對類型為jar文件的input進行遍歷input.jarInputs.each { JarInput jarInput ->//jar文件一般是第三方依賴庫jar文件// 重命名輸出文件(同目錄copyFile會沖突)def jarName = jarInput.namedef md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())if (jarName.endsWith(".jar")) {jarName = jarName.substring(0, jarName.length() - 4)}//生成輸出路徑 + md5Namedef dest = outputProvider.getContentLocation(jarName + md5Name,jarInput.contentTypes, jarInput.scopes, Format.JAR)//這里執行字節碼的注入,不操作字節碼的話也要將輸入路徑拷貝到輸出路徑FileUtils.copyFile(jarInput.file, dest)}}}注意input的類型,分為“文件夾”和“jar文件”,”文件夾”里面的就是我們寫的類對應的class文件,jar文件一般為第三方庫。此時,能成功運行,但是這里我們沒有注入任何代碼。
Transform類我們實現了,那么如何調用的呢?調用方式如下:
public class PluginImpl implements Plugin<Project>{void apply(Project project){def android = project.extensions.findByType(AppExtension);android.registerTransform(new InsertTransform())} }(2)監控每一個Task任務執行
在我們的工程目錄中我們可以看到還有一個TaskListener.groovy類,內容如下:
package com.davispluginsimport org.gradle.BuildListener import org.gradle.BuildResult import org.gradle.api.Task import org.gradle.api.execution.TaskExecutionListener import org.gradle.api.initialization.Settings import org.gradle.api.invocation.Gradle import org.gradle.api.tasks.TaskStatepublic class TaskListener implements TaskExecutionListener, BuildListener {private static final String TAG = "[DAVIS] ";/*** 此類可以監控每一個task的執行開始和結束,以及工程build的情況*/public TaskListener(){}@Overridevoid beforeExecute(Task task) {println(TAG + "task before : " + task.getName())}/*** 比如,我們要在packageRelease這個task任務執行完后,做一些操作,* 我們就可以在此方法中判斷* @param task* @param taskState*/@Overridevoid afterExecute(Task task, TaskState taskState) {println(TAG + "task after : " + task.getName())if(task.getName().equals("packageRelease")){//做自己的任務}}@Overridevoid buildFinished(BuildResult result) {//項目build完成之后,會調用此方法println(TAG + "build finished.")}@Overridevoid buildStarted(Gradle gradle) {println(TAG + "build started.")}@Overridevoid projectsEvaluated(Gradle gradle) {println(TAG + "project evaluated.")}@Overridevoid projectsLoaded(Gradle gradle) {println(TAG + "project loaded.")}@Overridevoid settingsEvaluated(Settings settings) {println(TAG + "setting evaluated.")} }調用方式:
public class PluginImpl implements Plugin<Project>{void apply(Project project){project.gradle.addListener(new TaskListener())} }這個類是做啥用的呢,此類可以用來監控每一個Task任務的執行情況,比如我們在打apk包的過程中,其實就是調用了一連串的Task任務。下面是我們在未使用插件的情況下打一個release包過程中Gradle Console輸出的日志:
Executing tasks: [:app:assembleRelease]Configuration on demand is an incubating feature. Incremental java compilation is an incubating feature. :app:preBuild UP-TO-DATE :app:preReleaseBuild UP-TO-DATE :app:checkReleaseManifest :app:prepareReleaseDependencies :app:compileReleaseAidl UP-TO-DATE :app:compileReleaseRenderscript UP-TO-DATE :app:generateReleaseBuildConfig UP-TO-DATE :app:mergeReleaseShaders UP-TO-DATE :app:compileReleaseShaders UP-TO-DATE :app:generateReleaseAssets UP-TO-DATE :app:mergeReleaseAssets UP-TO-DATE :app:generateReleaseResValues UP-TO-DATE :app:generateReleaseResources UP-TO-DATE :app:mergeReleaseResources UP-TO-DATE :app:processReleaseManifest UP-TO-DATE :app:processReleaseResources UP-TO-DATE :app:generateReleaseSources UP-TO-DATE :app:incrementalReleaseJavaCompilationSafeguard UP-TO-DATE :app:compileReleaseJavaWithJavac UP-TO-DATE :app:compileReleaseNdk UP-TO-DATE :app:compileReleaseSources UP-TO-DATE :app:lintVitalRelease :app:prePackageMarkerForRelease :app:transformClassesWithDexForRelease To run dex in process, the Gradle daemon needs a larger heap. It currently has approximately 1365 MB. For faster builds, increase the maximum heap size for the Gradle daemon to more than 2048 MB. To do this set org.gradle.jvmargs=-Xmx2048M in the project gradle.properties. For more information see https://docs.gradle.org/current/userguide/build_environment.html :app:mergeReleaseJniLibFolders UP-TO-DATE :app:transformNative_libsWithMergeJniLibsForRelease UP-TO-DATE :app:processReleaseJavaRes UP-TO-DATE :app:transformResourcesWithMergeJavaResForRelease UP-TO-DATE :app:validateExternalOverrideSigning :app:packageRelease UP-TO-DATE :app:zipalignRelease UP-TO-DATE :app:assembleReleaseBUILD SUCCESSFULTotal time: 5.557 secs那么我們使用了該插件之后輸出的日志是怎樣的那,如下:
Executing tasks: [:app:assembleRelease]Configuration on demand is an incubating feature. Incremental java compilation is an incubating feature. [DAVIS] project evaluated. :app:preBuild [DAVIS] task before : preBuild :app:preBuild UP-TO-DATE [DAVIS] task after : preBuild :app:preReleaseBuild [DAVIS] task before : preReleaseBuild :app:preReleaseBuild UP-TO-DATE [DAVIS] task after : preReleaseBuild :app:checkReleaseManifest [DAVIS] task before : checkReleaseManifest [DAVIS] task after : checkReleaseManifest :app:prepareReleaseDependencies [DAVIS] task before : prepareReleaseDependencies [DAVIS] task after : prepareReleaseDependencies :app:compileReleaseAidl [DAVIS] task before : compileReleaseAidl :app:compileReleaseAidl UP-TO-DATE [DAVIS] task after : compileReleaseAidl :app:compileReleaseRenderscript [DAVIS] task before : compileReleaseRenderscript :app:compileReleaseRenderscript UP-TO-DATE [DAVIS] task after : compileReleaseRenderscript :app:generateReleaseBuildConfig [DAVIS] task before : generateReleaseBuildConfig :app:generateReleaseBuildConfig UP-TO-DATE [DAVIS] task after : generateReleaseBuildConfig :app:mergeReleaseShaders [DAVIS] task before : mergeReleaseShaders :app:mergeReleaseShaders UP-TO-DATE [DAVIS] task after : mergeReleaseShaders :app:compileReleaseShaders [DAVIS] task before : compileReleaseShaders :app:compileReleaseShaders UP-TO-DATE [DAVIS] task after : compileReleaseShaders :app:generateReleaseAssets [DAVIS] task before : generateReleaseAssets :app:generateReleaseAssets UP-TO-DATE [DAVIS] task after : generateReleaseAssets :app:mergeReleaseAssets [DAVIS] task before : mergeReleaseAssets :app:mergeReleaseAssets UP-TO-DATE [DAVIS] task after : mergeReleaseAssets :app:generateReleaseResValues [DAVIS] task before : generateReleaseResValues :app:generateReleaseResValues UP-TO-DATE [DAVIS] task after : generateReleaseResValues :app:generateReleaseResources [DAVIS] task before : generateReleaseResources :app:generateReleaseResources UP-TO-DATE [DAVIS] task after : generateReleaseResources :app:mergeReleaseResources [DAVIS] task before : mergeReleaseResources :app:mergeReleaseResources UP-TO-DATE [DAVIS] task after : mergeReleaseResources :app:processReleaseManifest [DAVIS] task before : processReleaseManifest :app:processReleaseManifest UP-TO-DATE [DAVIS] task after : processReleaseManifest :app:processReleaseResources [DAVIS] task before : processReleaseResources :app:processReleaseResources UP-TO-DATE [DAVIS] task after : processReleaseResources :app:generateReleaseSources [DAVIS] task before : generateReleaseSources :app:generateReleaseSources UP-TO-DATE [DAVIS] task after : generateReleaseSources :app:incrementalReleaseJavaCompilationSafeguard [DAVIS] task before : incrementalReleaseJavaCompilationSafeguard :app:incrementalReleaseJavaCompilationSafeguard UP-TO-DATE [DAVIS] task after : incrementalReleaseJavaCompilationSafeguard :app:compileReleaseJavaWithJavac [DAVIS] task before : compileReleaseJavaWithJavac :app:compileReleaseJavaWithJavac UP-TO-DATE [DAVIS] task after : compileReleaseJavaWithJavac :app:compileReleaseNdk [DAVIS] task before : compileReleaseNdk :app:compileReleaseNdk UP-TO-DATE [DAVIS] task after : compileReleaseNdk :app:compileReleaseSources [DAVIS] task before : compileReleaseSources :app:compileReleaseSources UP-TO-DATE [DAVIS] task after : compileReleaseSources :app:lintVitalRelease [DAVIS] task before : lintVitalRelease [DAVIS] task after : lintVitalRelease :app:prePackageMarkerForRelease [DAVIS] task before : prePackageMarkerForRelease [DAVIS] task after : prePackageMarkerForRelease :app:transformClassesWithDavisPluginForRelease [DAVIS] task before : transformClassesWithDavisPluginForRelease :app:transformClassesWithDavisPluginForRelease UP-TO-DATE [DAVIS] task after : transformClassesWithDavisPluginForRelease :app:transformClassesWithDexForRelease [DAVIS] task before : transformClassesWithDexForRelease To run dex in process, the Gradle daemon needs a larger heap. It currently has approximately 1365 MB. For faster builds, increase the maximum heap size for the Gradle daemon to more than 2048 MB. To do this set org.gradle.jvmargs=-Xmx2048M in the project gradle.properties. For more information see https://docs.gradle.org/current/userguide/build_environment.html [DAVIS] task after : transformClassesWithDexForRelease :app:mergeReleaseJniLibFolders [DAVIS] task before : mergeReleaseJniLibFolders :app:mergeReleaseJniLibFolders UP-TO-DATE [DAVIS] task after : mergeReleaseJniLibFolders :app:transformNative_libsWithMergeJniLibsForRelease [DAVIS] task before : transformNative_libsWithMergeJniLibsForRelease :app:transformNative_libsWithMergeJniLibsForRelease UP-TO-DATE [DAVIS] task after : transformNative_libsWithMergeJniLibsForRelease :app:processReleaseJavaRes [DAVIS] task before : processReleaseJavaRes :app:processReleaseJavaRes UP-TO-DATE [DAVIS] task after : processReleaseJavaRes :app:transformResourcesWithMergeJavaResForRelease [DAVIS] task before : transformResourcesWithMergeJavaResForRelease :app:transformResourcesWithMergeJavaResForRelease UP-TO-DATE [DAVIS] task after : transformResourcesWithMergeJavaResForRelease :app:validateExternalOverrideSigning [DAVIS] task before : validateExternalOverrideSigning [DAVIS] task after : validateExternalOverrideSigning :app:packageRelease [DAVIS] task before : packageRelease :app:packageRelease UP-TO-DATE [DAVIS] task after : packageRelease :app:zipalignRelease [DAVIS] task before : zipalignRelease :app:zipalignRelease UP-TO-DATE [DAVIS] task after : zipalignRelease :app:assembleRelease [DAVIS] task before : assembleRelease [DAVIS] task after : assembleReleaseBUILD SUCCESSFULTotal time: 3.6 secs [DAVIS] build finished.從上面的日志我們可以看出,我們可以在項目打包前、某個Task任務執行前或執行后以及整個項目打包完成后來做自己想做的事了。
GitHub源碼地址:https://github.com/881205wzs/GradlePluginDemo
源碼下載地址:https://download.csdn.net/download/wangzhongshun/11010210
總結
以上是生活随笔為你收集整理的Android Studio 自定义Gradle Plugin的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: aux是什么意思(aux在车上有什么用)
- 下一篇: Android设计模式之——责任链模式