Gradle插件、注解、javapoet和asm實(shí)戰(zhàn)

實(shí)戰(zhàn)庫ImplLoader的介紹

首先來介紹一下實(shí)戰(zhàn)項(xiàng)目的所解決的問題 : 當(dāng)一個(gè)Android工程中如果已經(jīng)使用不同的module來做業(yè)務(wù)隔離。那我們就可能有這種需求,module1想實(shí)例化一個(gè)module2的類,一般要怎么解決呢?

  • module1依賴module2
  • module2的這個(gè)類沉到底層庫,然后module1module2都使用這個(gè)底層庫。
  • ....等

下面來介紹一個(gè)小庫 : ImplLoader。可以很方便解決這個(gè)問題。只需這樣使用即可:

  1. 使用@Impl標(biāo)記需要被加載的類
//`module2`中的類:
@Impl(name = "module2_text_view")
public class CommonView extends AppCompatTextView {

}
  1. 使用 ImplLoader.getImpl("module2_text_view") 來獲取這個(gè)類
public class Module1Page extends LinearLayout {
    public Module1Page(@NonNull Context context) {
        super(context);
        init();
    }

    private void init() {
        //根據(jù)name,獲取需要加載的類
        View module1Tv = ImplLoader.getView(getContext(), "module2_text_view");
        addView(module1Tv);
    }
}
  1. 初始化ImplLoader
    ImplLoader.init()

庫的代碼放在: https://github.com/SusionSuc/ImplLoader

為什么要寫這個(gè)庫 ?

主要是為了練手

在閱讀WMRouterARouter源碼時(shí)發(fā)現(xiàn)這兩個(gè)庫都用到了自定義注解、自定義gradle插件、Gradle Transfrom API、javapoet和asm庫。而我對于這些知識(shí)很多我只是了解個(gè)大概,或者壓根就沒聽說過。
因此ImplLoader這個(gè)庫主要是用來熟悉這個(gè)知識(shí)的。當(dāng)然這個(gè)庫的實(shí)現(xiàn)思路主要參考WMRouterARouter

庫的實(shí)現(xiàn)原理

用下面這種圖概括一下:

ImplLoader實(shí)現(xiàn)原理.png

其實(shí)整個(gè)庫代碼并不多,不過實(shí)現(xiàn)起來用到的東西不少,如果一些你使用的不熟悉,可以先看一下:

https://github.com/SusionSuc/AdvancedAndroid

這個(gè)庫是用來總結(jié)我這兩年Android所學(xué)和對自我提高的一個(gè)庫。里面的文章我寫的很用心,會(huì)一直頻繁更新。

下面簡單過一下ImplLoader的實(shí)現(xiàn)代碼(只看主流程):

定義@Impl注解

@Retention(RetentionPolicy.RUNTIME)
public @interface Impl {
    String name() default "";
}

編譯時(shí)注解處理器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());
    }


     // 新產(chǎn)生一個(gè)類
    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());
    }

運(yùn)行時(shí)反射實(shí)例化ImplLoaderHelp.class,并調(diào)用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文件中會(huì)調(diào)用
    fun registerImpl(implName: String, implClass: Class<*>) {
        implMap.put(implName, implClass)
    }

    ... 獲取實(shí)例相關(guān)方法....
}

實(shí)現(xiàn)過程中遇到的一些問題

注解處理器庫的創(chuàng)建

整個(gè)項(xiàng)目我是建了一個(gè)AndroidProject。因?yàn)樽⒔鈳熘粫?huì)在編譯的時(shí)候用到,因此我單獨(dú)建了一個(gè)Android Library庫,用來存放注解處理相關(guān)代碼??墒窃趯懙臅r(shí)候,發(fā)現(xiàn)找不到javax.annotation下注解相關(guān)類。后來發(fā)現(xiàn)原因是新建的Android Library是不會(huì)包含這寫庫的,需要新建一個(gè)Java Library

如何調(diào)試注解處理器 和 Gradle Transfrom

注解處理器代碼編寫完了?怎么調(diào)試呢? 具體參考 : https://blog.csdn.net/jeasonlzy/article/details/74273851 這篇文章,我把如何調(diào)試注解處理器這段搬過來:

  1. 在項(xiàng)目根目錄下的gradle.properties中添加如下兩行配置
org.gradle.daemon=true //記得把創(chuàng)建項(xiàng)目自動(dòng)創(chuàng)建寫的那個(gè)注釋掉
org.gradle.jvmargs=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5006
  1. 打開運(yùn)行配置,添加一個(gè)遠(yuǎn)程調(diào)試如下, 其中name可以任意取,port端口號(hào)就是上面一步指定的端口號(hào)。
添加processer.png
  1. 切換運(yùn)行配置到切換剛剛創(chuàng)建的processor,然后點(diǎn)擊debug按鈕
運(yùn)行processer.jpg
  1. 最后,在我們需要調(diào)試的地方打上斷點(diǎn),然后再次點(diǎn)擊編譯按鈕(小錘子按鈕),即可進(jìn)入斷點(diǎn)

上面這4步也適用于調(diào)試Gradle Transform

上傳自定義的 Gradle Transform插件到本地目錄然后引用

編寫完成Gradle Transform Plugin之后我怎么使用了?上傳到maven然后依賴? 不太現(xiàn)實(shí),因?yàn)槲乙恢闭{(diào)試。最后決定這樣解決:

  1. 把插件上傳到工程下的一個(gè)目錄(作為maven倉庫)
apply plugin : 'maven'

group 'com.susion.loaderplugin'
version '0.0.1'

uploadArchives {
    repositories {
        flatDir {
            name "../localRepo"
            dir "../localRepo/libs"
        }
    }
}
  1. 在主工程的build.gradle引入本地maven庫
buildscript {
    repositories {
        flatDir {
            name 'localRepo'
            dir "localRepo/libs/implloader"
        }   
    }
    dependencies {
        classpath 'com.susion.loaderplugin:loaderplugin:0.0.1'
    }
}
  1. 在demo引入插件
apply plugin: 'com.susion.loaderplugin'

經(jīng)過這樣操作后,整個(gè)插件開發(fā)將會(huì)非常方便。

支持kotlin

對于java文件,如果要處理其中的注解,我們可以這樣引入我們的注解處理器:

    annotationProcessor project(":compiler")

但是當(dāng)我在module中創(chuàng)建了一個(gè)kotlin文件,并標(biāo)記@Impl后我發(fā)現(xiàn)。我自定義的注解處理器并不能掃描到kotlin文件上的注解。如果想要讓注解處理器在kotlin文件上生效需要對帶有kotlin代碼的工程,加上kotlin的注解處理插件:

apply plugin: 'kotlin-kapt'  //引入 kotlin kapt

dependencies {
    .....
    kapt project(':compiler')
}

庫的上傳

決定將庫上傳到maven,但因?yàn)?code>ImplLoader的實(shí)現(xiàn)涉及到4個(gè)庫 loaderplugin、loadercoreannotation-interfacecompiler。因此想要使用一個(gè)統(tǒng)一的腳本來上傳這4個(gè)庫到binary

首先在主項(xiàng)目的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'

    }
}

使用下面這個(gè)腳本統(tǒng)一做上傳:

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
    }
}

即每個(gè)庫的 artifactedId為:project.name

最后在對于的module中使用這個(gè)腳本即可。

還有一些小問題這里先不講述了。歡迎關(guān)注我的 : https://github.com/SusionSuc/AdvancedAndroid

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容