Gradle外掛、註解、javapoet和asm實戰
實戰庫 ImplLoader
的介紹
首先來介紹一下實戰專案的所解決的問題 : 當一個Android工程中如果已經使用不同的module來做業務隔離。那我們就可能有這種需求,module1想例項化一個module2的類,一般要怎麼解決呢?
-
module1
依賴module2
- 把
module2
的這個類沉到底層庫,然後module1
和module2
都使用這個底層庫。 - ....等
下面來介紹一個小庫 : ImplLoader
。可以很方便解決這個問題。只需這樣使用即可:
- 使用
@Impl
標記需要被載入的類
//`module2`中的類: @Impl(name = "module2_text_view") public class CommonView extends AppCompatTextView { }
- 使用
ImplLoader.getImpl("module2_text_view")
來獲取這個類
public class Module1Page extends LinearLayout { public Module1Page(@NonNull Context context) { super(context); init(); } private void init() { //根據name,獲取需要載入的類 View module1Tv = ImplLoader.getView(getContext(), "module2_text_view"); addView(module1Tv); } }
- 初始化
ImplLoader
ImplLoader.init()
庫的程式碼放在: ofollow,noindex">https://github.com/SusionSuc/ImplLoader
為什麼要寫這個庫 ?
主要是為了練手
在閱讀 WMRouter
和 ARouter
原始碼時發現這兩個庫都用到了 自定義註解
、 自定義gradle外掛
、 Gradle Transfrom API
、 javapoet和asm庫
。而我對於這些知識很多我只是瞭解個大概,或者壓根就沒聽說過。
因此 ImplLoader
這個庫主要是用來熟悉這個知識的。當然這個庫的實現思路主要參考 WMRouter
和 ARouter
。
庫的實現原理
用下面這種圖概括一下:

ImplLoader實現原理.png
其實整個庫程式碼並不多,不過實現起來用到的東西不少,如果一些你使用的不熟悉,可以先看一下:
https://github.com/SusionSuc/AdvancedAndroid
這個庫是用來總結我這兩年Android所學和對自我提高的一個庫。裡面的文章我寫的很用心,會一直頻繁更新。
下面簡單過一下 ImplLoader
的實現程式碼(只看主流程):
定義 @Impl
註解
@Retention(RetentionPolicy.RUNTIME) public @interface Impl { String name() default ""; }
編譯時註解處理器 ImplAnnotationProcessor
, 掃描 @Impl
,並生成 ImplInfo_XXX.java
//ImplAnnotationProcessor.process() @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { ..... HashMap<String, ImplAnnotationInfo> implMap = new HashMap<>(); //用來儲存掃描到的註解資訊 for (Element implElement : roundEnv.getElementsAnnotatedWith(Impl.class)) { ImplAnnotationInfo implAnnotationInfo = getImplAnnotationInfo((TypeElement) implElement); implMap.put(implAnnotationInfo.name, implAnnotationInfo); } //生成 ImplInfo_xxx.java new ImplClassProtocolGenerate(elementsUitls, filer).generateImplProtocolClass(implMap); return true; } //生成 ImplInfo_xxx.java void generateImplProtocolClass(HashMap<String, ImplAnnotationInfo> implMap) { TypeSpec.Builder implInfoSpec = getImplInfoSpec(); MethodSpec.Builder implInfoMethodSpec = getImplInfoMethodSpec(); for (String implName : implMap.keySet()) { CodeBlock registerBlock = getImplInfoInitCode(implMap.get(implName)); implInfoMethodSpec.addCode(registerBlock); } implProtocolSpec.addMethod(implInfoMethodSpec.build()); writeImplProtocolCode(implInfoSpec.build()); }
Gradle Transfrom
掃描生成的 ImplInfo_XXX.java
檔案,並生成 ImplLoaderHelp.class
//ImplLoaderTransform.java @Override public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { Set<String> implInfoClasses = new HashSet<>(); for (TransformInput input : transformInvocation.getInputs()) { input.getJarInputs().forEach(jarInput -> { try { File jarFile = jarInput.getFile(); File dst = transformInvocation.getOutputProvider().getContentLocation( jarInput.getName(), jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR); implInfoClasses.addAll(InsertImplInfoCode.getImplInfoClassesFromJar(jarFile)); FileUtils.copyFile(jarFile, dst);//必須要把輸入,copy到輸出,不然接下來沒有辦法處理 } catch (IOException e) { } }); input.getDirectoryInputs().forEach(directoryInput -> { //...... }); } File dest = transformInvocation.getOutputProvider().getContentLocation( "ImplLoader", TransformManager.CONTENT_CLASS, ImmutableSet.of(QualifiedContent.Scope.PROJECT), Format.DIRECTORY); InsertImplInfoCode.insertImplInfoInitMethod(implInfoClasses, dest.getAbsolutePath()); } // 新產生一個類 public static void insertImplInfoInitMethod(Set<String> implInfoClasses, String outputDirPath) { ..... ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS); ClassVisitor cv = new ClassVisitor(Opcodes.ASM5, writer) {}; String className = ProtocolConstants.IMPL_LOADER_HELP_CLASS.replace('.', '/'); cv.visit(50, Opcodes.ACC_PUBLIC, className, null, "java/lang/Object", null); MethodVisitor mv = cv.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC,ProtocolConstants.IMPL_LOADER_HELP_INIT_METHOD, "()V", null, null); mv.visitCode(); for (String clazz : implInfoClasses) { mv.visitMethodInsn(Opcodes.INVOKESTATIC, clazz.replace('.', '/'), ProtocolConstants.IMPL_INFO_CLASS_INIT_METHOD, "()V", false); } mv.visitMaxs(0, 0); mv.visitInsn(Opcodes.RETURN); mv.visitEnd(); cv.visitEnd(); File dest = new File(outputDirPath, className + SdkConstants.DOT_CLASS); dest.getParentFile().mkdirs(); new FileOutputStream(dest).write(writer.toByteArray()); }
執行時反射例項化 ImplLoaderHelp.class
,並呼叫 init
方法,來載入 @Impl
註冊的類
object ImplLoader { //儲存 @Impl註冊的類 private val implMap = HashMap<String, Class<*>>() @JvmStatic fun init() { try { Class.forName(ProtocolConstants.IMPL_LOADER_HELP_CLASS) .getMethod(ProtocolConstants.IMPL_LOADER_HELP_INIT_METHOD) .invoke(null) } catch (e: Exception) {} } //在生成的 ImplInfo_XX.java檔案中會呼叫 fun registerImpl(implName: String, implClass: Class<*>) { implMap.put(implName, implClass) } ... 獲取例項相關方法.... }
實現過程中遇到的一些問題
註解處理器庫的建立
整個專案我是建了一個 AndroidProject
。因為註解庫只會在編譯的時候用到,因此我單獨建了一個 Android Library
庫,用來存放註解處理相關程式碼。可是在寫的時候,發現找不到 javax.annotation
下註解相關類。後來發現原因是新建的 Android Library
是不會包含這寫庫的,需要新建一個 Java Library
如何除錯註解處理器 和 Gradle Transfrom
註解處理器程式碼編寫完了?怎麼除錯呢? 具體參考 : https://blog.csdn.net/jeasonlzy/article/details/74273851 這篇文章,我把如何除錯註解處理器這段搬過來:
- 在專案根目錄下的gradle.properties中新增如下兩行配置
org.gradle.daemon=true //記得把建立專案自動建立寫的那個註釋掉 org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5006
- 開啟執行配置,新增一個遠端除錯如下, 其中name可以任意取,port埠號就是上面一步指定的埠號。

新增processer.png
- 切換執行配置到切換剛剛建立的processor,然後點選debug按鈕

執行processer.jpg
- 最後,在我們需要除錯的地方打上斷點,然後再次點選編譯按鈕(小錘子按鈕),即可進入斷點
上面這4步也適用於除錯Gradle Transform
上傳自定義的 Gradle Transform外掛到本地目錄然後引用
編寫完成 Gradle Transform Plugin
之後我怎麼使用了?上傳到 maven
然後依賴? 不太現實,因為我要一直除錯。最後決定這樣解決:
- 把外掛上傳到工程下的一個目錄(作為maven倉庫)
apply plugin : 'maven' group 'com.susion.loaderplugin' version '0.0.1' uploadArchives { repositories { flatDir { name "../localRepo" dir "../localRepo/libs" } } }
- 在主工程的
build.gradle
引入本地maven庫
buildscript { repositories { flatDir { name 'localRepo' dir "localRepo/libs/implloader" } } dependencies { classpath 'com.susion.loaderplugin:loaderplugin:0.0.1' } }
- 在demo引入外掛
apply plugin: 'com.susion.loaderplugin'
經過這樣操作後,整個外掛開發將會非常方便。
支援kotlin
對於java檔案,如果要處理其中的註解,我們可以這樣引入我們的註解處理器:
annotationProcessor project(":compiler")
但是當我在module中建立了一個 kotlin
檔案,並標記 @Impl
後我發現。我自定義的註解處理器並不能掃描到 kotlin
檔案上的註解。如果想要讓註解處理器在kotlin檔案上生效需要對帶有kotlin程式碼的工程,加上kotlin的註解處理外掛:
apply plugin: 'kotlin-kapt'//引入 kotlin kapt dependencies { ..... kapt project(':compiler') }
庫的上傳
決定將庫上傳到 maven
,但因為 ImplLoader
的實現涉及到4個庫 loaderplugin
、 loadercore
、 annotation-interface
和 compiler
。因此想要使用一個統一的指令碼來上傳這4個庫到 binary
首先在主專案的 build.gradle
中引入 binary
外掛依賴
buildscript { repositories { jcenter() mavenCentral() } dependencies { ..... classpath 'com.github.dcendents:android-maven-gradle-plugin:latest.release' classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.6' } }
使用下面這個指令碼統一做上傳:
apply plugin: 'com.github.dcendents.android-maven' apply plugin: 'com.jfrog.bintray' group = "com.susion.implloader" version = "1.0.0" //一些敏感的資訊放在 local.properties 中 def getPropertyFromLocalProperties(key) { File file = project.rootProject.file('local.properties') if (file.exists()) { Properties properties = new Properties() properties.load(file.newDataInputStream()) return properties.getProperty(key) } } bintray { user = getPropertyFromLocalProperties("bintray.user") key = getPropertyFromLocalProperties("bintray.apikey") configurations = ['archives'] pkg { repo = 'maven' name = "${project.group}:${project.name}" userOrg = "${project.name}" licenses = ['Apache-2.0'] websiteUrl = 'https://github.com/SusionSuc' vcsUrl = '' publish = true } }
即每個庫的 artifactedId為: project.name
。
最後在對於的module中使用這個指令碼即可。
還有一些小問題這裡先不講述了。歡迎關注我的 : https://github.com/SusionSuc/AdvancedAndroid