手寫簡化EventBus之註解處理器方式,理解框架核心原理

前言

自前一篇文章:手寫簡化EventBus,理解框架核心原理(反射實現方式)寫完後,一直在研究註解處理器實現方式,中間又有其他事情耽擱了,所以到今天才補上這篇文章。
此篇文章是在上篇反射方式實現的源碼的基礎上進行更改實現的,所以如果還沒看上篇文章的可以先瀏覽下,能夠更快的瞭解脈絡和源碼結構,能夠更快的進入主題,更快理解。

註解處理器(APT)

顧名思義,APT就是註解處理器,其是Annotation Processing Tool的簡稱。它是javac的一個工具,用來在編譯期掃描和處理註解,通過註解來生成文件(通常是java文件)。即以註解作爲橋樑,通過預先規定好的代碼生成規則來自動生成 Java 文件。這些生成的java文件會同其手動編寫的java代碼一樣會被javac編譯。
簡單的流程框圖如下:即原來需要在程序運行時獲取的方法集合,現在可以在編譯時註解生成的新類A中的某個方法獲取了。此種方式可以減輕初始化時反射帶來的性能損耗,當然此損耗是在項目較大,註冊較多時比較明顯。
在這裏插入圖片描述雖然流程圖上畫的是調用註解處理器,但是Android工程編譯的時候JVM怎麼找到我們自定義的註解處理器?這個時候就要用到Java SPI機制(這裏只引出概念,有需要的可以一起查閱學習,深入研究原理,這裏只當做黑箱,使用這個能力)。就是在annotationprocess模塊的resources目錄下新建META-INF/services,然後新建File,名稱javax.annotation.processing.Processor,文件內容就是我們自定義註解處理器的全限定名com.example.AnnotationProcessor,谷歌官方也出品了一個開源庫Auto-Service,通過註解@AutoService(Processor.class)可以省略上面配置的步驟。AutoService會自動在META-INF文件夾下生成Processor配置信息文件,該文件裏就是實現該服務接口的具體實現類。而當外部程序裝配這個模塊的時候,就能通過該jar包META-INF/services/裏的配置文件找到具體的實現類名,並裝載實例化,完成模塊的注入。
其中AbstractProcessor 的主要方法如下:

public abstract class AbstractProcessor implements Processor {
//獲取要處理的註解的類型(本例中是@Subscribe)可通過註解方式賦值
public Set<String> getSupportedAnnotationTypes() {
	SupportedAnnotationTypes sat = this.getClass().getAnnotation(SupportedAnnotationTypes.class);
……
}
// 獲取java需要支持的版本,可註解方式賦值
public SourceVersion getSupportedSourceVersion() {
        SupportedSourceVersion ssv = this.getClass().getAnnotation(SupportedSourceVersion.class);
        ……
}
// 初始化註解處理器
public synchronized void init(ProcessingEnvironment processingEnv) {
     ……
}
// 要實現的process方法,在實現類中此方法主要是生成新的類文件。
public abstract boolean process(Set<? extends TypeElement> annotations,RoundEnvironment roundEnv);
}

實際上,生成這個類文件A也有兩種方式,一個就是用註解處理器自帶的processingEnv裏的生成文件的方法。另一種即是使用框架javapoet。通過javapoet這個名字翻譯,java 詩人,你能想象寫代碼會有多優雅。
其中processingEnv自帶的方式,也是EventBus3.0源碼目前使用的方式生成代碼示例爲:

BufferedWriter writer = null;
        try {
            JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(index);
            int period = index.lastIndexOf('.');
            String myPackage = period > 0 ? index.substring(0, period) : null;
            String clazz = index.substring(period + 1);
            writer = new BufferedWriter(sourceFile.openWriter());
            if (myPackage != null) {
                writer.write("package " + myPackage + ";\n\n");
            }
            writer.write("import org.greenrobot.eventbus.meta.SimpleSubscriberInfo;\n");
            writer.write("import org.greenrobot.eventbus.meta.SubscriberMethodInfo;\n");
            writer.write("import org.greenrobot.eventbus.meta.SubscriberInfo;\n");
            writer.write("import org.greenrobot.eventbus.meta.SubscriberInfoIndex;\n\n");
            writer.write("import org.greenrobot.eventbus.ThreadMode;\n\n");
            writer.write("import java.util.HashMap;\n");
            writer.write("import java.util.Map;\n\n");
            writer.write("/** This class is generated by EventBus, do not edit. */\n");
            writer.write("public class " + clazz + " implements SubscriberInfoIndex {\n");
            writer.write("    private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;\n\n");
            writer.write("    static {\n");
            writer.write("        SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>();\n\n");
            writeIndexLines(writer, myPackage);
            writer.write("    }\n\n");
            writer.write("    private static void putIndex(SubscriberInfo info) {\n");
            writer.write("        SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info);\n");
            writer.write("    }\n\n");
            writer.write("    @Override\n");
            writer.write("    public SubscriberInfo getSubscriberInfo(Class<?> subscriberClass) {\n");
            writer.write("        SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);\n");
            writer.write("        if (info != null) {\n");
            writer.write("            return info;\n");
            writer.write("        } else {\n");
            writer.write("            return null;\n");
            writer.write("        }\n");
            writer.write("    }\n");
            writer.write("}\n");
        } catch (IOException e) {
            throw new RuntimeException("Could not write source for " + index, e);
        } finally {

此方式每行都要用函數調用,且爲了生成的java類文件的美觀性還要注意縮進等,在有規律性的添加源碼時不能很好的減少工作量等。
而如果使用javapoet,如下爲其github上自帶的簡單的源碼示例,更多類型的寫法可以參考javapoet github地址https://github.com/square/javapoet
要生成的類文件樣式爲:

package com.example.helloworld;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!");
  }
}

使用JavaPoet框架生成類文件使用的方法:

MethodSpec main = MethodSpec.methodBuilder("main")
    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
    .returns(void.class)
    .addParameter(String[].class, "args")
    .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
    .addMethod(main)
    .build();

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
    .build();

javaFile.writeTo(System.out);

是不是完全是java編碼一樣的方式,使用建造者模式對一個類、方法、字段等進行生成。

手寫框架之註解處理器

新建module

  • annotations
    此module是java library的module,即其build.gradle中顯示apply plugin: ‘java-library’。
    主要是自定義註解和註解方法的java bean部分。
  • apt_processor
    註解處理器,新建MyEventBusAnnotationProcessor實現AbstractProcessor,實現其抽象方法process,生成新的類文件。

以上兩個新建module爲何要java library呢,因爲這個AbstractProcessor本就是java的東西,而java的module只能依賴java library,如annotations,不能依賴android類型的library。但是反過來,android的library可以依賴java的library,即原app module和myeventBus library都可以依賴上述兩個新建的library。

gradle配置

在使用註解處理器時,編譯會出現不認識中文註釋GBK編碼等問題,需要在對應build.gradle加入支持utf8編碼。

tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}

另外的編譯失敗即有gradle版本等的問題,也有java的版本的問題。本例子以測試ok,其他組合的gradle版本讀者可以自己嘗試。
java版本需要8,即jdk8

sourceCompatibility = "8"
targetCompatibility = "8"

註解處理器的module, apt_processor中的build.gradle需要配置爲如下,目前的最新版本。

implementation project(path: ':annotations')
implementation group: 'com.google.auto.service', name: 'auto-service', version: '1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
implementation group: 'com.squareup', name: 'javapoet', version: '1.12.1'

另外在app的主module裏除了其他library的依賴還要加入annotationProcessor配置。

implementation project(':myeventbus')
implementation project(':annotations')
annotationProcessor project(':apt_processor')

EventBus註解處理器調用方式

首先告訴大家一個事實,我們平常使用的EventBus基本都是使用反射方式的。新的3.0版本的實現方式增加了註解方法,但是使用方式是有不同的。
可以查閱官方說明:https://greenrobot.org/eventbus/documentation/subscriber-index/

Subscriber Index
    Since EventBus 3.0
//使用Subscriber Index,避免使用反射查找所有的訂閱者方法,其註解處理器會在編譯時查找他們。
Using a subscriber index avoids expensive look-ups of subscriber methods at run time using reflection. Instead, the EventBus annotation processor looks them up at build time.
//推薦在android應用正式發佈版本使用這個index方法,更快並且避免反射引起的崩潰
It is recommended to use the index for Android apps in production. It is faster and avoids crashes due to reflection (see reported issues due to NoClassDefFoundError)

即使用時,需要通過建造者方法,把註解處理器自動生成的方法MyEventBusIndex,通過addIndex方法傳入,並且將EventBus的靜態實例instance即getDefault中的單例方法賦值,這樣在下次使用getDefault方法時,已經是有通過build設置了index的單例,而不會重新創建。以下爲官網使用說明。
How to use the index

Build the project at least once to generate the index class specified with eventBusIndex.

Then, e.g. in your Application class, use EventBus.builder().addIndex(indexInstance) to pass an instance of the index class to EventBus.

EventBus eventBus = EventBus.builder().addIndex(new MyEventBusIndex()).build();

Use EventBusBuilder.installDefaultEventBus() to set the EventBus with index as the instance returned by EventBus.getDefault().

EventBus.builder().addIndex(new MyEventBusIndex()).installDefaultEventBus();
// Now the default instance uses the given index. Use it like this:
EventBus eventBus = EventBus.getDefault();

同樣的,我們也使用同樣的方式去將我們自己設計生成的模板類傳入我們的建造者類中。

設計模板類

在上節註解處理器中,有描述生成新類A,那這個A在生成之前,我們肯定要設計下這個類應該是什麼樣子的。功能是什麼。
首先,整體的UML框架類圖如下,是在原反射類型的例子基礎上更改的,應用調用MyEventBus註冊訂閱,如果是反射類型實現,則調用ReflectInvoke方法獲取所有訂閱處理方法並調用,後者實現了MethodHandle接口的兩個方法getAllSubscribedMethods和invokeMethod,這個流程比較簡單。有需要查看源碼的讀者請點擊查看:https://github.com/qingdaofu1/ZephyrBus
在這裏插入圖片描述但是,涉及到了註解處理器後,我們需要知道一個前提,即那個要生成的模板類A,是生成在app主module中的。這就帶來了一個問題,本來app module依賴myeventBus module,現在在後者的AptAnnotationInvoke實現方法又要調用app module生成的方法。 經過查看EventBus的源碼後發現,其使用了一個建造者模式,將依賴方法傳入。這裏我們一樣學習他的方法,採用建造者模式,所謂的生成的模板類A,繼承Methodhandle接口並實現其接口方法。這樣MyEventBusBuilder建造者即有了從app module生成類中方法的實例,然後生成MyEventBus實例。

  1. 設計模板類AptMethodFinderTemplate
    在使用註解處理器生成新類之前,我們需要設計要生成什麼類型的類,然後按圖索驥,分析模板中的方法,查閱javapoet的工具類進行實現。
    但是本步驟是要確保此類可用,即原MyEventBus.getDefault().register(this);方式改爲如下方式註冊。
//註解處理器代碼的模板類
AptMethodFinderTemplate aptMethodFinder = new AptMethodFinderTemplate();
        //註解處理調用方式
MyEventBus.builder().setMethodHandle(aptMethodFinder).build().register(this);

如下爲模板類,需要使用javapoet生成的目標類,類名根據需要自己更改,其中有些信息肯定不是類似反射能獲取到的一樣,因爲編譯時並沒有實例,只能獲取到方法名等一類的信息。

public class AptMethodFinderTemplate implements MethodHandle {

    private static final Map<Object, List<SubscribedMethod>> aptMap = new HashMap<>();

    static {
        aptMap.put(com.example.zephyrbus.MainActivity.class, findMethodsInMainActivity());
    }

    @Override
    public List<SubscribedMethod> getAllSubscribedMethods(Object subscriber) {
        return aptMap.get(subscriber);
    }

    @Override
    public void invokeMethod(Subscription subscription, Object event) {

    }

    private static List<SubscribedMethod> findMethodsInMainActivity(){
        List<SubscribedMethod> subscribedMethods = new ArrayList<>();
        subscribedMethods.add(new SubscribedMethod(com.example.zephyrbus.MainActivity.class, com.example.zephyrbus.Event.WorkEvent.class, ThreadMode.POSTING, 0, "onEvent"));
        subscribedMethods.add(new SubscribedMethod(com.example.zephyrbus.MainActivity.class, com.example.zephyrbus.Event.ViewEvent.class, ThreadMode.MAIN, 0, "handleView"));
        return subscribedMethods;
    }
}

經過測試,此模板類正確的收集了所有的訂閱者方法,並且通過其中集合保存信息順利調用到了對應的方法。但是這個是手動蒐集的,自己騙自己,只是用於測試。如果有十個類,幾十個類註冊了eventbus,這就不合適了。正確的途徑即通過註解處理器自動生成,即通過註解處理器,自動根據註解的類文件信息生成對應的類。

  1. 編譯自動生成代碼

在上一篇手寫EventBus的反射實現篇,反射是需要在程序運行時反射獲取類中的方法和註解信息的,那是因爲進程運行後,JVM能拿到所有的類信息。但是編譯時能獲取麼,當然可以,編譯能檢查出語法錯誤,肯定能獲取到所有有依賴的類的信息。
上述模板類中,和具體類無關的自帶、方法可以直接生成,但是涉及具體類和訂閱者方法,則需要獲取。在我們的自定義註解處理器類MyEventBusAnnotationProcessor中的process方法中有個參數RoundEnvironment,其是javax.annotation.processing包路徑,即java註解處理的類,其接口方法getElementsAnnotatedWith可以獲取到添加了對應註解的方法信息。
殊途同歸,無論反射還是註解處理器,我們的目的是一樣的,能拿到什麼樣的米,就下什麼樣的飯,雖然與反射獲取信息不同,但是仍能達到目的。
接下來就是javapoet的一些使用要點了,當然github其頁面有詳細說明。這裏只根據我們目前模板需要的,點出幾個。。示例。

JavaPoet的常用類,可以用來生成方法、字段、類、註解、構造方法、接口、枚舉、匿名內部類、javadoc等。讀者可以根據需要去學習其他用法。具體參見其github地址https://github.com/square/javapoet

TypeSpec:用於生成類、接口、枚舉對象的類
MethodSpec:用於生成方法對象的類
ParameterSpec:用於生成參數對象的類
AnnotationSpec:用於生成註解對象的類
FieldSpec:用於配置生成成員變量的類
ClassName:通過包名和類名生成的對象,在JavaPoet中相當於爲其指定Class
ParameterizedTypeName:通過MainClass和IncludeClass生成包含泛型的Class
JavaFile:控制生成的Java文件的輸出的類。
CodeBlock:生成靜態代碼塊,本例中有使用,即static{}包括其內容
其還支持映射
$L for Literals:字符串連接的方法beginControlFlow() 和 addStatement是分散開的,操作較多。
$S for Strings:當輸出的代碼包含字符串的時候, 可以使用 $S 表示一個 string
$T for Types:使用Java內置的類型會使代碼比較容易理解。JavaPoet極大的支持這些類型,通過 $T 進行映射,會自動import聲明.
$N for Names:使用 $N 可以引用另外一個通過名字生成的聲明

本例中,
如下Javapoet方法,生成的是第一行註釋掉部分的代碼。雖然看着代碼量劇增,不如原方案簡單,但是javapoet的優勢是優雅,哈哈。 其提供的接口都熟悉的話,使用確實很簡單。

//        private static final Map<Object, List<SubscribedMethod>> aptMap = new HashMap<>();
//
        ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get(List.class, SubscribedMethod.class);
        ClassName map = ClassName.get("java.util", "Map");
        ClassName object = ClassName.get("java.lang", "Object");
        FieldSpec aptMap = FieldSpec.builder(ParameterizedTypeName.get(map, object, parameterizedTypeName), "aptMap")
                .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
                .initializer("new $T<>()", HashMap.class)
                .build();

然後實現MethodHandle兩個接口方法

    /*@Override
    public List<SubscribedMethod> getAllSubscribedMethods(Object subscriber) {
        return aptMap.get(subscriber);
    }*/

        MethodSpec getSubscribMethod = MethodSpec.methodBuilder("getAllSubscribedMethods")
                .addModifiers(Modifier.PUBLIC)
                .returns(ParameterizedTypeName.get(List.class, SubscribedMethod.class))
                .addParameter(Object.class, "subscriber")
                .addCode("return aptMap.get(subscriber);")
                .build();

         /*@Override
        public void invokeMethod(Subscription subscription, Object event) {
            // TODO: 2020/4/19
        }*/
        MethodSpec invokeMethod = MethodSpec.methodBuilder("invokeMethod")
                .addModifiers(Modifier.PUBLIC)
                .returns(TypeName.VOID)
                .addParameter(Subscription.class, "subscription")
                .addParameter(Object.class, "event")
                //.addCode("return aptMap.get(subscriber);")
                .build();

創建類信息,這裏類名設爲AptMethodFinder。

//類建造者 TypeSpec.Builder aptMethodFinder = TypeSpec.classBuilder("AptMethodFinder")
        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
        .addSuperinterface(MethodHandle.class)
        .addField(aptMap)
        .addMethod(getSubscribMethod)
        .addMethod(invokeMethod);

之後,根據註冊的訂閱者類信息,動態生成需要的方法和靜態代碼塊。最後生成類文件。

        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Subscribe.class);
        //整個工程進程中所有的註解了Subscribe的方法
        for (Element element : elements) {
            //註解了Subscribe的方法元素  強轉爲可執行方法元素
            ExecutableElement executableElement = (ExecutableElement) element;
            //獲取這個方法所在類元素,強轉爲類元素
            TypeElement typeElement = (TypeElement) executableElement.getEnclosingElement();
            //獲取全路徑類名
            String qualifiedName = typeElement.getQualifiedName().toString();
            //將這個循環裏的這個元素分類,分別放到對應<類,類中所有訂閱者方法>集合中,先從集合中獲取這個類中的所有需要創建的方法。沒有則新建
            CreateMethod createMethod = mCachedCreateMethod.get(qualifiedName);
            if (createMethod == null) {
                createMethod = new CreateMethod(typeElement);
                mCachedCreateMethod.put(qualifiedName, createMethod);
            }

            //簡單方法名,非全路徑
            String methodName = executableElement.getSimpleName().toString();
            //放入方法名和方法的map
            createMethod.putElement(methodName, executableElement);

        }

        CodeBlock.Builder codeBlock = CodeBlock.builder();
        //遍歷所有類中所有的註解方法
        for (String key : mCachedCreateMethod.keySet()) {
            //獲取一個類  key 中的所有要創建的方法。
            CreateMethod createMethod = mCachedCreateMethod.get(key);
            //創建方法並添加到類中,比如 MainActivity中,這個方法
            //
//        private static List<SubscribedMethod> findMethodsInMainActivity(){
//            List<SubscribedMethod> subscribedMethods = new ArrayList<>();
//            subscribedMethods.add(new SubscribedMethod(com.example.zephyrbus.MainActivity.class, com.example.zephyrbus.Event.WorkEvent.class, ThreadMode.POSTING, 0, "onEvent"));
//            subscribedMethods.add(new SubscribedMethod(com.example.zephyrbus.MainActivity.class, com.example.zephyrbus.Event.ViewEvent.class, ThreadMode.MAIN, 0, "handleView"));
//            return subscribedMethods;
//        }
            aptMethodFinder.addMethod(createMethod.generateMethod());

            //        static {
//            aptMap.put(com.example.zephyrbus.MainActivity.class, findMethodsInMainActivity());
//        }
            codeBlock.add("aptMap.put($L.class, $L());\n", key, createMethod.getMethodName());
        }
        //將靜態代碼塊加入到類文件中。     類建造者build後變爲類
        TypeSpec typeSpec = aptMethodFinder.addStaticBlock(codeBlock.build()).build();

        JavaFile javaFile = JavaFile.builder("com.example.zephyrbus", typeSpec)
                .build();
        try {
            javaFile.writeTo(filer);
        } catch (IOException e) {
            e.printStackTrace();
        }

生成的類AptMethodFinder,肯定其中方法與原AptMethodFinderTemplate方法是一樣的作用,爲了保證生成類不與手動編寫的類產生衝突,需要針對性取名,比如很多第三方框架在類名中會添加$或0 1等。

源碼測試

在反射篇已經對測試代碼進行了說明,這裏只要進行相同的測試即可,只不過實現方式不同。
如果是建造者方式註冊,

AptMethodFinder aptMethodFinder = new AptMethodFinder();
        //註解處理調用方式
MyEventBus.builder().setMethodHandle(aptMethodFinder).build().register(this);

在這裏插入圖片描述而使用反射調用方式,可以看到對應的tag又是反射調用的了。

//反射調用方式
MyEventBus.getDefault().register(this);

在這裏插入圖片描述

後記

經過對註解處理器和javapoet的學習與測試,清楚的瞭解了註解處理器的作用,對以後理解和學習其他第三方框架有了較好的基礎。但是部分原理性的還需要繼續深度挖掘,像javapoet還有很多方法沒有使用到,希望以後在其他框架的學習、手寫測試時能夠學習到。
畢竟,看到的都不是自己的,能寫出來,才代表瞭解。
有需要查看源碼的讀者請點擊查看:https://github.com/qingdaofu1/ZephyrBus

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章