Android元件化開發實踐(九):自定義Gradle外掛
本文緊接著前一章 ofollow,noindex">Android元件化開發實踐(八):元件生命週期如何實現自動註冊管理 ,主要講解怎麼通過自定義外掛來實現元件生命週期的自動註冊管理。
1. 採用groovy建立外掛
新建一個Java Library module,命名為lifecycle-plugin,刪除 src->main 下面的java目錄,新建一個 groovy 目錄,在groovy目錄下建立類似java的package,在 src->main 下面建立一個 resources 目錄,在 resources 目錄下依次建立 META-INF/gradle-plugins 目錄,最後在該目錄下建立一個名為 com.hm.plugin.lifecycle.properties 的文字檔案,檔名是你要定義的外掛名,按需自定義即可。最後的工程結構如圖所示:

修改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將外掛釋出到本地的指令碼配置,根據自己的要求來修改 uploadArchives { repositories.mavenDeployer { pom.version = '1.0.0' pom.artifactId = 'hmlifecyclepluginlocal' pom.groupId = 'com.heima.iou' repository(url: "file:///Users/hjy/.m2/repository/") } }
這裡有幾點需要說明的是:
- 通常都是採用groovy語言來建立gradle plugin的,groovy是相容java的,你完全可以採用java來編寫外掛。關於groovy語言,瞭解一些基礎語法就足夠支撐我們去編寫外掛了。
- 在 src/main/resources/META-INF/gradle-plugins 目錄下定義外掛宣告,*.properties檔案的檔名就是外掛名稱。
2. 實現Plugin介面
要編寫一個外掛是很簡單的,只需實現Plugin介面即可。
package com.hm.iou.lifecycle.plugin import com.android.build.gradle.AppExtension import org.gradle.api.Plugin import org.gradle.api.Project class LifeCyclePlugin implements Plugin<Project>{ @Override void apply(Project project) { println "------LifeCycle plugin entrance-------" } }
接著在com.hm.plugin.lifecycle.properties檔案裡增加配置:
implementation-class=com.hm.iou.lifecycle.plugin.LifeCyclePlugin
其中 implementation-class 的值為 Plugin 介面的實現類的全限定類名,至此為止一個最簡單的外掛編寫好了,它的功能很簡單,僅僅是在控制檯列印一句文字而已。
我們通過maven將該外掛釋出到本地的maven倉庫裡,釋出成功後,我們在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-------"來,這說明我們的自定義外掛成功了。
講到這裡可以看到,按這個步驟實現一個gradle外掛是很簡單的,它並沒有我們想象中那麼高深莫測,你也可以自豪地說我會製作gradle外掛了。
3. Gradle Transform
然而前面這個外掛並沒有什麼卵用,它僅僅只是在編譯時,在控制檯列印一句話而已。那麼怎麼通過外掛在打包前去掃描所有的class檔案呢,幸運的是官方給我們提供了 Gradle Transform 技術,簡單來說就是能夠讓開發者在專案構建階段即由class到dex轉換期間修改class檔案,Transform階段會掃描所有的class檔案和資原始檔,具體技術我這裡不詳細展開,下面通過虛擬碼部分說下我的思路。
//只需要繼承Transform類即可 class LifeCycleTransform extends Transform { Project project LifeCycleTransform(Project project) { this.project = project } //該Transform的名稱,自定義即可,只是一個標識 @Override String getName() { return "LifeCycleTransform" } //該Transform支援掃描的檔案型別,分為class檔案和資原始檔,我們這裡只處理class檔案的掃描 @Override Set<QualifiedContent.ContentType> getInputTypes() { return TransformManager.CONTENT_CLASS } //Transfrom的掃描範圍,我這裡掃描整個工程,包括當前module以及其他jar包、aar檔案等所有的class @Override Set<? super QualifiedContent.Scope> getScopes() { return TransformManager.SCOPE_FULL_PROJECT } //是否增量掃描 @Override boolean isIncremental() { return true } @Override void 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 的類,是我們要找的目標class,直接通過class的名稱來判斷,也可以再加上包名的判斷,會更嚴謹點 if (ScanUtil.isTargetProxyClass(file)) { //如果是我們自己生產的代理類,儲存該類的類名 appLikeProxyClassList.add(file.name) } } } //Transform掃描的class檔案是輸入檔案(input),有輸入必然會有輸出(output),處理完成後需要將輸入檔案拷貝到一個輸出目錄下去, //後面打包將class檔案轉換成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轉換為outputs def jarName = jarInput.name def md5 = DigestUtils.md5Hex(jarInput.file.getAbsolutePath()) if (jarName.endsWith(".jar")) { jarName = jarName.substring(0, jarName.length() - 4) } //獲取輸出路徑下的jar包名稱,必須這樣獲取,得到的輸出路徑名不能重複,否則會被覆蓋 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包,只處理有我們業務邏輯的jar包 if (ScanUtil.shouldProcessPreDexJar(src.absolutePath)) { //掃描jar包的核心程式碼在這裡,主要做2件事情: //1.掃描該jar包裡有沒有實現IAppLike介面的代理類; //2.掃描AppLifeCycleManager這個類在哪個jar包裡,並記錄下來,後面需要在該類裡動態注入位元組碼; 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.通過前面的步驟,我們已經掃描到所有實現了 IAppLike介面的代理類; //2.後面需要在 AppLifeCycleManager 這個類的初始化方法裡,動態注入位元組碼; //3.將所有 IAppLike 介面的代理類,通過類名進行反射呼叫例項化 //這樣最終生成的apk包裡,AppLifeCycleManager呼叫init()方法時,已經可以載入所有元件的生命週期類了 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是應用生命週期框架初始化方法呼叫類 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是否是我們的目標類 * * @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包裡進行程式碼注入 * * @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 = null while (enumeration.hasMoreElements()) { //遍歷這個jar包裡的所有class檔案項 JarEntry jarEntry = enumeration.nextElement() //class檔案的名稱,這裡是全路徑類名,包名之間以"/"分隔 String entryName = jarEntry.getName() if (entryName == REGISTER_CLASS_FILE_NAME) { //標記這個jar包包含 AppLifeCycleManager.class //掃描結束後,我們會生成註冊程式碼到這個檔案裡 FILE_CONTAINS_INIT_CLASS = destFile } else { //通過包名來判斷,嚴謹點還可以加上類名字首、字尾判斷 //通過APT生成的類,都有統一的字首、字尾 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介面實現類,在外掛中註冊該Transfrom:
class LifeCyclePlugin implements Plugin<Project>{ @Override void apply(Project project) { println "------LifeCycle plugin entrance-------" def android = project.extensions.getByType(AppExtension) android.registerTransform(new LifeCycleTransform(project)) } }
前面的程式碼裡,先註釋掉LifeCycleTransform類裡的AppLikeCodeInjector相關程式碼,這塊我們後面再講。我們再新建一個Android Library module,在該module裡建立 ModuleCAppLike、ModuleDAppLike,同樣都實現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包等,會當做下一個Transform的inputs傳遞過去。
4. 通過ASM動態修改位元組碼
到現在,我們只剩下最後一步了,那就是如何注入程式碼了。ASM 是一個 Java 位元組碼操控框架,它能被用來動態生成類或者增強既有類的功能。我這裡對ASM不做詳細介紹了,主要是介紹使用ASM動態注入程式碼的思路。
首先,我們修改一下AppLifeCycleManager類,增加動態注入位元組碼的入口方法:
/** * 通過外掛載入 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()方法呼叫時會先執行。通過前面Transform步驟之後,我們現在的目標是把程式碼動態插入到loadAppLike()方法裡,下面這段程式碼是我們期望插入後的結果:
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"); }
這樣在初始化時,就已經知道要載入哪些生命週期類,來看看具體實現方法,關於ASM不瞭解的地方,需要先搞清楚其使用方法再來閱讀:
class AppLikeCodeInjector { //掃描出來的所有 IAppLike 類 List<String> proxyAppLikeClassList AppLikeCodeInjector(List<String> list) { proxyAppLikeClassList = list } void execute() { println("開始執行ASM方法======>>>>>>>>") File srcFile = ScanUtil.FILE_CONTAINS_INIT_CLASS //建立一個臨時jar檔案,要修改注入的位元組碼會先寫入該檔案裡 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動態注入位元組碼 if (ScanUtil.REGISTER_CLASS_FILE_NAME == entryName) { println "insert register code to class >> " + entryName ClassReader classReader = new ClassReader(inputStream) // 構建一個ClassWriter物件,並設定讓系統自動計算棧和本地變數大小 ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS) ClassVisitor classVisitor = new AppLikeClassVisitor(classWriter) //開始掃描class檔案 classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES) byte[] bytes = classWriter.toByteArray() //將注入過位元組碼的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包裡已經包含了我們注入的位元組碼了 optJar.renameTo(srcFile) } //插入位元組碼的邏輯,都在這個類裡面 class AppLikeClassVisitor extends ClassVisitor { AppLikeClassVisitor(ClassVisitor classVisitor) { super(Opcodes.ASM5, classVisitor) } @Override MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exception) { println "visit method: " + name MethodVisitor mv = super.visitMethod(access, name, desc, signature, exception) //找到 AppLifeCycleManager裡的loadAppLike()方法,我們在這個方法裡插入位元組碼 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) } @Override protected void onMethodEnter() { super.onMethodEnter() println "-------onMethodEnter------" //遍歷插入位元組碼,其實就是在 loadAppLike() 方法裡插入類似registerAppLike("");的位元組碼 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); }) } @Override protected void onMethodExit(int opcode) { super.onMethodExit(opcode) println "-------onMethodEnter------" } } }
最後重新編譯外掛再執行,驗證結果。
這裡有個比較困難的地方,就是需要使用ASM編寫class位元組碼。我這裡推薦一個比較好用的方法:
- 將要注入的java原始碼先寫出來;
- 通過javac編譯出class檔案;
- 通過 asm-all.jar 反編譯該class檔案,可得到所需的ASM注入程式碼;
執行命令如下:
java -classpath "asm-all.jar" org.objectweb.asm.util.ASMifier com/hm/lifecycle/api/AppLifeCycleManager.class
從中找到loadAppLike()方法位元組碼處,這樣通過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. 小結
結合前一章我們基本上實現了自動註冊載入元件的生命週期管理類,做到了無侵入式的服務註冊,離我們的徹底元件化解耦更近一步了。本文有些地方借鑑了阿里的路由框架ARouter,其基本思路是一致的,弄懂了這些也基本上就弄懂了ARouter的實現原理 ,原理弄清楚了之後,在此基礎上咱們寫出自己的框架也不是什麼難事了。
原始碼地址: https://github.com/houjinyun/Android-AppLifecycleMgr
原始碼已經託管到github上了, 有興趣的可以跟我留言,互相交流學習進步。