Android组件化开发实践(九):自定义Gradle插件
本文緊接著前一章Android組件化開發(fā)實踐(八):組件生命周期如何實現(xiàn)自動注冊管理,主要講解怎么通過自定義插件來實現(xiàn)組件生命周期的自動注冊管理。
1. 采用groovy創(chuàng)建插件
新建一個Java Library module,命名為lifecycle-plugin,刪除 src->main 下面的java目錄,新建一個groovy目錄,在groovy目錄下創(chuàng)建類似java的package,在 src->main 下面創(chuàng)建一個 resources 目錄,在resources目錄下依次創(chuàng)建 META-INF/gradle-plugins 目錄,最后在該目錄下創(chuàng)建一個名為 com.hm.plugin.lifecycle.properties的文本文件,文件名是你要定義的插件名,按需自定義即可。最后的工程結(jié)構(gòu)如圖所示:
修改module的build.gradle文件,引入groovy插件等:
apply plugin: 'java-library' apply plugin: 'groovy' apply plugin: 'maven'dependencies {implementation fileTree(dir: 'libs', include: ['*.jar'])compile gradleApi()compile localGroovy()compile 'com.android.tools.build:transform-api:1.5.0'compile 'com.android.tools.build:gradle:3.0.1' }sourceCompatibility = "1.7" targetCompatibility = "1.7"//通過maven將插件發(fā)布到本地的腳本配置,根據(jù)自己的要求來修改 uploadArchives {repositories.mavenDeployer {pom.version = '1.0.0'pom.artifactId = 'hmlifecyclepluginlocal'pom.groupId = 'com.heima.iou'repository(url: "file:///Users/hjy/.m2/repository/")} }這里有幾點需要說明的是:
2. 實現(xiàn)Plugin接口
要編寫一個插件是很簡單的,只需實現(xiàn)Plugin接口即可。
package com.hm.iou.lifecycle.pluginimport com.android.build.gradle.AppExtension import org.gradle.api.Plugin import org.gradle.api.Projectclass LifeCyclePlugin implements Plugin<Project>{@Overridevoid apply(Project project) {println "------LifeCycle plugin entrance-------"} }接著在com.hm.plugin.lifecycle.properties文件里增加配置:
implementation-class=com.hm.iou.lifecycle.plugin.LifeCyclePlugin其中implementation-class的值為Plugin接口的實現(xiàn)類的全限定類名,至此為止一個最簡單的插件編寫好了,它的功能很簡單,僅僅是在控制臺打印一句文本而已。
我們通過maven將該插件發(fā)布到本地的maven倉庫里,發(fā)布成功后,我們在app module里引入該插件,修改app module目錄下的build.gradle文件,增加如下配置:
apply plugin: 'com.android.application' //引入自定義插件,插件名與前面的*.properties文件的文件名是一致的 apply plugin: 'com.hm.plugin.lifecycle' buildscript {repositories {google()jcenter()//自定義插件maven地址,替換成你自己的maven地址maven { url 'file:///Users/hjy/.m2/repository/' }}dependencies {//通過maven加載自定義插件classpath 'com.heima.iou:hmlifecyclepluginlocal:1.0.0'} }我們build一下工程,在Gradle Console里會打印出"------LifeCycle plugin entrance-------"來,這說明我們的自定義插件成功了。
講到這里可以看到,按這個步驟實現(xiàn)一個gradle插件是很簡單的,它并沒有我們想象中那么高深莫測,你也可以自豪地說我會制作gradle插件了。
3. Gradle Transform
然而前面這個插件并沒有什么卵用,它僅僅只是在編譯時,在控制臺打印一句話而已。那么怎么通過插件在打包前去掃描所有的class文件呢,幸運的是官方給我們提供了 Gradle Transform技術(shù),簡單來說就是能夠讓開發(fā)者在項目構(gòu)建階段即由class到dex轉(zhuǎn)換期間修改class文件,Transform階段會掃描所有的class文件和資源文件,具體技術(shù)我這里不詳細(xì)展開,下面通過偽代碼部分說下我的思路。
//只需要繼承Transform類即可 class LifeCycleTransform extends Transform {Project projectLifeCycleTransform(Project project) {this.project = project}//該Transform的名稱,自定義即可,只是一個標(biāo)識@OverrideString getName() {return "LifeCycleTransform"}//該Transform支持掃描的文件類型,分為class文件和資源文件,我們這里只處理class文件的掃描@OverrideSet<QualifiedContent.ContentType> getInputTypes() {return TransformManager.CONTENT_CLASS}//Transfrom的掃描范圍,我這里掃描整個工程,包括當(dāng)前module以及其他jar包、aar文件等所有的class@OverrideSet<? super QualifiedContent.Scope> getScopes() {return TransformManager.SCOPE_FULL_PROJECT}//是否增量掃描@Overrideboolean isIncremental() {return true}@Overridevoid transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs,TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {println "\nstart to transform-------------->>>>>>>"def appLikeProxyClassList = []//inputs就是所有掃描到的class文件或者是jar包,一共2種類型inputs.each { TransformInput input ->//1.遍歷所有的class文件目錄input.directoryInputs.each { DirectoryInput directoryInput ->//遞歸掃描該目錄下所有的class文件if (directoryInput.file.isDirectory()) {directoryInput.file.eachFileRecurse {File file ->//形如 Heima$$****$$Proxy.class 的類,是我們要找的目標(biāo)class,直接通過class的名稱來判斷,也可以再加上包名的判斷,會更嚴(yán)謹(jǐn)點if (ScanUtil.isTargetProxyClass(file)) {//如果是我們自己生產(chǎn)的代理類,保存該類的類名appLikeProxyClassList.add(file.name)}}}//Transform掃描的class文件是輸入文件(input),有輸入必然會有輸出(output),處理完成后需要將輸入文件拷貝到一個輸出目錄下去,//后面打包將class文件轉(zhuǎn)換成dex文件時,直接采用的就是輸出目錄下的class文件了。//必須這樣獲取輸出路徑的目錄名稱def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)FileUtils.copyDirectory(directoryInput.file, dest)}//2.遍歷查找所有的jar包input.jarInputs.each { JarInput jarInput ->println "\njarInput = ${jarInput}"//與處理class文件一樣,處理jar包也是一樣,最后要將inputs轉(zhuǎn)換為outputsdef jarName = jarInput.namedef md5 = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())if (jarName.endsWith(".jar")) {jarName = jarName.substring(0, jarName.length() - 4)}//獲取輸出路徑下的jar包名稱,必須這樣獲取,得到的輸出路徑名不能重復(fù),否則會被覆蓋def dest = outputProvider.getContentLocation(jarName + md5, jarInput.contentTypes, jarInput.scopes, Format.JAR)if (jarInput.file.getAbsolutePath().endsWith(".jar")) {File src = jarInput.file//先簡單過濾掉 support-v4 之類的jar包,只處理有我們業(yè)務(wù)邏輯的jar包if (ScanUtil.shouldProcessPreDexJar(src.absolutePath)) {//掃描jar包的核心代碼在這里,主要做2件事情://1.掃描該jar包里有沒有實現(xiàn)IAppLike接口的代理類;//2.掃描AppLifeCycleManager這個類在哪個jar包里,并記錄下來,后面需要在該類里動態(tài)注入字節(jié)碼;List<String> list = ScanUtil.scanJar(src, dest)if (list != null) {appLikeProxyClassList.addAll(list)}}}//將輸入文件拷貝到輸出目錄下FileUtils.copyFile(jarInput.file, dest)}}println ""appLikeProxyClassList.forEach({fileName ->println "file name = " + fileName})println "\n包含AppLifeCycleManager類的jar文件"println ScanUtil.FILE_CONTAINS_INIT_CLASS.getAbsolutePath()println "開始自動注冊"//1.通過前面的步驟,我們已經(jīng)掃描到所有實現(xiàn)了 IAppLike接口的代理類;//2.后面需要在 AppLifeCycleManager 這個類的初始化方法里,動態(tài)注入字節(jié)碼;//3.將所有 IAppLike 接口的代理類,通過類名進(jìn)行反射調(diào)用實例化//這樣最終生成的apk包里,AppLifeCycleManager調(diào)用init()方法時,已經(jīng)可以加載所有組件的生命周期類了new AppLikeCodeInjector(appLikeProxyClassList).execute()println "transform finish----------------<<<<<<<\n"} }我們來看看ScanUtil類里的代碼邏輯:
class ScanUtil {static final PROXY_CLASS_PREFIX = "Heima\$\$"static final PROXY_CLASS_SUFFIX = "\$\$Proxy.class"//注意class文件名中的包名是以“/”分隔開,而不是“.”分隔的,這個包名是我們通過APT生成的所有 IAppLike 代理類的包名static final PROXY_CLASS_PACKAGE_NAME = "com/hm/iou/lifecycle/apt/proxy"//AppLifeCycleManager是應(yīng)用生命周期框架初始化方法調(diào)用類static final REGISTER_CLASS_FILE_NAME = "com/hm/lifecycle/api/AppLifeCycleManager.class"//包含生命周期管理初始化類的文件,即包含 com.hm.lifecycle.api.AppLifeCycleManager 類的class文件或者jar文件static File FILE_CONTAINS_INIT_CLASS/*** 判斷該class是否是我們的目標(biāo)類** @param file* @return*/static boolean isTargetProxyClass(File file) {if (file.name.endsWith(PROXY_CLASS_SUFFIX) && file.name.startsWith(PROXY_CLASS_PREFIX)) {return true}return false}/*** 掃描jar包里的所有class文件:* 1.通過包名識別所有需要注入的類名* 2.找到AppLifeCycleManager類所在的jar包,后面我們會在該jar包里進(jìn)行代碼注入** @param jarFile* @param destFile* @return*/static List<String> scanJar(File jarFile, File destFile) {def file = new JarFile(jarFile)Enumeration<JarEntry> enumeration = file.entries()List<String> list = nullwhile (enumeration.hasMoreElements()) {//遍歷這個jar包里的所有class文件項JarEntry jarEntry = enumeration.nextElement()//class文件的名稱,這里是全路徑類名,包名之間以"/"分隔String entryName = jarEntry.getName()if (entryName == REGISTER_CLASS_FILE_NAME) {//標(biāo)記這個jar包包含 AppLifeCycleManager.class//掃描結(jié)束后,我們會生成注冊代碼到這個文件里FILE_CONTAINS_INIT_CLASS = destFile} else {//通過包名來判斷,嚴(yán)謹(jǐn)點還可以加上類名前綴、后綴判斷//通過APT生成的類,都有統(tǒng)一的前綴、后綴if (entryName.startsWith(PROXY_CLASS_PACKAGE_NAME)) {if (list == null) {list = new ArrayList<>()}list.addAll(entryName.substring(entryName.lastIndexOf("/") + 1))}}}return list}static boolean shouldProcessPreDexJar(String path) {return !path.contains("com.android.support") && !path.contains("/android/m2repository")}}修改Plugin接口實現(xiàn)類,在插件中注冊該Transfrom:
class LifeCyclePlugin implements Plugin<Project>{@Overridevoid apply(Project project) {println "------LifeCycle plugin entrance-------"def android = project.extensions.getByType(AppExtension)android.registerTransform(new LifeCycleTransform(project))} }前面的代碼里,先注釋掉LifeCycleTransform類里的AppLikeCodeInjector相關(guān)代碼,這塊我們后面再講。我們再新建一個Android Library module,在該module里創(chuàng)建 ModuleCAppLike、ModuleDAppLike,同樣都實現(xiàn)IAppLike接口并采用@AppLifeCycle作為注解。最后采用最新的插件重新build一下工程,看看Gradle Console里的輸出信息。
file name = Heima$$ModuleCAppLike$$Proxy.class file name = Heima$$ModuleDAppLike$$Proxy.class file name = Heima$$ModuleAAppLike$$Proxy.class file name = Heima$$ModuleBAppLike$$Proxy.class包含AppLifeCycleManager類的jar文件 /Users/hjy/Desktop/heima/code/gitlab/HM-AppLifeCycleMgr/app/build/intermediates/transforms/LifeCycleTransform/debug/17.jar可以看到,在Transform過程中,我們找到了ModuleAAppLike、ModuleBAppLike、ModuleCAppLike、ModuleDAppLike這4個類的代理類,以及AppLifeCycleManager這個class文件所在的jar包。
在app->build->intermediates->transforms中,可以看到所有的Transform,包括我們剛才自定義的Transform。從上圖中可以看到,這里的0.jar、1.jar、2.jar等等,都是通過outputProvider.getContentLocation()方法來生成的,這個Transform目錄下的class文件、jar包等,會當(dāng)做下一個Transform的inputs傳遞過去。
4. 通過ASM動態(tài)修改字節(jié)碼
到現(xiàn)在,我們只剩下最后一步了,那就是如何注入代碼了。ASM 是一個 Java 字節(jié)碼操控框架,它能被用來動態(tài)生成類或者增強(qiáng)既有類的功能。我這里對ASM不做詳細(xì)介紹了,主要是介紹使用ASM動態(tài)注入代碼的思路。
首先,我們修改一下AppLifeCycleManager類,增加動態(tài)注入字節(jié)碼的入口方法:
/*** 通過插件加載 IAppLike 類*/private static void loadAppLike() {}//通過反射去加載 IAppLike 類的實例private static void registerAppLike(String className) {if (TextUtils.isEmpty(className))return;try {Object obj = Class.forName(className).getConstructor().newInstance();if (obj instanceof IAppLike) {APP_LIKE_LIST.add((IAppLike) obj);}} catch (Exception e) {e.printStackTrace();}}/*** 初始化** @param context*/public static void init(Context context) {//通過插件加載 IAppLike 類loadAppLike();Collections.sort(APP_LIKE_LIST, new AppLikeComparator());for (IAppLike appLike : APP_LIKE_LIST) {appLike.onCreate(context);}}相比之前,這里增加了一個loadAppLike()方法,在init()方法調(diào)用時會先執(zhí)行。通過前面Transform步驟之后,我們現(xiàn)在的目標(biāo)是把代碼動態(tài)插入到loadAppLike()方法里,下面這段代碼是我們期望插入后的結(jié)果:
private static void loadAppLike() {registerAppLike("com.hm.iou.lifecycle.apt.proxy.Heima$$ModuleAAppLike$$Proxy");registerAppLike("com.hm.iou.lifecycle.apt.proxy.Heima$$ModuleBAppLike$$Proxy");registerAppLike("com.hm.iou.lifecycle.apt.proxy.Heima$$ModuleCAppLike$$Proxy");registerAppLike("com.hm.iou.lifecycle.apt.proxy.Heima$$ModuleDAppLike$$Proxy"); }這樣在初始化時,就已經(jīng)知道要加載哪些生命周期類,來看看具體實現(xiàn)方法,關(guān)于ASM不了解的地方,需要先搞清楚其使用方法再來閱讀:
class AppLikeCodeInjector {//掃描出來的所有 IAppLike 類List<String> proxyAppLikeClassListAppLikeCodeInjector(List<String> list) {proxyAppLikeClassList = list}void execute() {println("開始執(zhí)行ASM方法======>>>>>>>>")File srcFile = ScanUtil.FILE_CONTAINS_INIT_CLASS//創(chuàng)建一個臨時jar文件,要修改注入的字節(jié)碼會先寫入該文件里def optJar = new File(srcFile.getParent(), srcFile.name + ".opt")if (optJar.exists())optJar.delete()def file = new JarFile(srcFile)Enumeration<JarEntry> enumeration = file.entries()JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(optJar))while (enumeration.hasMoreElements()) {JarEntry jarEntry = enumeration.nextElement()String entryName = jarEntry.getName()ZipEntry zipEntry = new ZipEntry(entryName)InputStream inputStream = file.getInputStream(jarEntry)jarOutputStream.putNextEntry(zipEntry)//找到需要插入代碼的class,通過ASM動態(tài)注入字節(jié)碼if (ScanUtil.REGISTER_CLASS_FILE_NAME == entryName) {println "insert register code to class >> " + entryNameClassReader classReader = new ClassReader(inputStream)// 構(gòu)建一個ClassWriter對象,并設(shè)置讓系統(tǒng)自動計算棧和本地變量大小ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS)ClassVisitor classVisitor = new AppLikeClassVisitor(classWriter)//開始掃描class文件classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)byte[] bytes = classWriter.toByteArray()//將注入過字節(jié)碼的class,寫入臨時jar文件里jarOutputStream.write(bytes)} else {//不需要修改的class,原樣寫入臨時jar文件里jarOutputStream.write(IOUtils.toByteArray(inputStream))}inputStream.close()jarOutputStream.closeEntry()}jarOutputStream.close()file.close()//刪除原來的jar文件if (srcFile.exists()) {srcFile.delete()}//重新命名臨時jar文件,新的jar包里已經(jīng)包含了我們注入的字節(jié)碼了optJar.renameTo(srcFile)}//插入字節(jié)碼的邏輯,都在這個類里面class AppLikeClassVisitor extends ClassVisitor {AppLikeClassVisitor(ClassVisitor classVisitor) {super(Opcodes.ASM5, classVisitor)}@OverrideMethodVisitor visitMethod(int access, String name,String desc, String signature,String[] exception) {println "visit method: " + nameMethodVisitor mv = super.visitMethod(access, name, desc, signature, exception)//找到 AppLifeCycleManager里的loadAppLike()方法,我們在這個方法里插入字節(jié)碼if ("loadAppLike" == name) {mv = new LoadAppLikeMethodAdapter(mv, access, name, desc)}return mv}}class LoadAppLikeMethodAdapter extends AdviceAdapter {LoadAppLikeMethodAdapter(MethodVisitor mv, int access, String name, String desc) {super(Opcodes.ASM5, mv, access, name, desc)}@Overrideprotected void onMethodEnter() {super.onMethodEnter()println "-------onMethodEnter------"//遍歷插入字節(jié)碼,其實就是在 loadAppLike() 方法里插入類似registerAppLike("");的字節(jié)碼proxyAppLikeClassList.forEach({proxyClassName ->println "開始注入代碼:${proxyClassName}"def fullName = ScanUtil.PROXY_CLASS_PACKAGE_NAME.replace("/", ".") + "." + proxyClassName.substring(0, proxyClassName.length() - 6)println "full classname = ${fullName}"mv.visitLdcInsn(fullName)mv.visitMethodInsn(INVOKESTATIC, "com/hm/lifecycle/api/AppLifeCycleManager", "registerAppLike", "(Ljava/lang/String;)V", false);})}@Overrideprotected void onMethodExit(int opcode) {super.onMethodExit(opcode)println "-------onMethodEnter------"}}}最后重新編譯插件再運行,驗證結(jié)果。
這里有個比較困難的地方,就是需要使用ASM編寫class字節(jié)碼。我這里推薦一個比較好用的方法:
執(zhí)行命令如下:
從中找到loadAppLike()方法字節(jié)碼處,這樣通過ASM注入代碼就比較簡單了:
{ mv = cw.visitMethod(ACC_PRIVATE + ACC_STATIC, "loadAppLike", "()V", null, null); mv.visitCode(); mv.visitLdcInsn("com.hm.iou.lifecycle.apt.proxy.Heima$$ModuleAAppLike$$Proxy"); mv.visitMethodInsn(INVOKESTATIC, "com/hm/lifecycle/api/AppLifeCycleManager", "registerAppLike", "(Ljava/lang/String;)V", false); mv.visitInsn(RETURN); mv.visitMaxs(1, 0); mv.visitEnd(); }5. 小結(jié)
結(jié)合前一章我們基本上實現(xiàn)了自動注冊加載組件的生命周期管理類,做到了無侵入式的服務(wù)注冊,離我們的徹底組件化解耦更近一步了。本文有些地方借鑒了阿里的路由框架ARouter,其基本思路是一致的,弄懂了這些也基本上就弄懂了ARouter的實現(xiàn)原理 ,原理弄清楚了之后,在此基礎(chǔ)上咱們寫出自己的框架也不是什么難事了。
源碼地址:https://github.com/houjinyun/Android-AppLifecycleMgr
源碼已經(jīng)托管到github上了, 有興趣的可以跟我留言,互相交流學(xué)習(xí)進(jìn)步。
系列文章
Android組件化開發(fā)實踐(一):為什么要進(jìn)行組件化開發(fā)?
Android組件化開發(fā)實踐(二):組件化架構(gòu)設(shè)計
Android組件化開發(fā)實踐(三):組件開發(fā)規(guī)范
Android組件化開發(fā)實踐(四):組件間通信問題
Android組件化開發(fā)實踐(五):組件生命周期管理
Android組件化開發(fā)實踐(六):老項目實施組件化
Android組件化開發(fā)實踐(七):開發(fā)常見問題及解決方案
Android組件化開發(fā)實踐(八):組件生命周期如何實現(xiàn)自動注冊管理
Android組件化開發(fā)實踐(九):自定義Gradle插件
總結(jié)
以上是生活随笔為你收集整理的Android组件化开发实践(九):自定义Gradle插件的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ES6 开发常用新特性以及简述ES7
- 下一篇: api与implementation的区