手写简化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

猜你喜欢

转载自blog.csdn.net/caizehui/article/details/106005874