转载

EventBus 3.0 从编译时注解分析源码

日常开发中可能很少会自己写注解处理器,但是很多开源库都用到了,如 ButterKnife、EventBus、Glide 等。因此我们必须要了解其原理,才能读懂其他大牛写的代码。

一般情况下编写编译时注解的项目时会分三个模块:

  • 注解模块:annotation module
  • 注解处理器模块:processor module
  • Api模块:api module

个人觉得如果注解不是特别多的话,还是把注解模块和 Api 模块合二为一更好,用户引入的时候就会像下面这个样子:

annotationProcessor "com.test.processor"
implementation "com.test.api"
复制代码

这样看起来会更清爽,无缘无故有多个依赖让人感觉有些麻烦。

1. 注解模块

写注解必须要知道 元注解 ,尤其是 @Retention@Target ,这里简单介绍下。

**@Retention **有三个枚举类型:

RetentionPolicy.SOURCE 表示注解只保留在源文件,当 Java 文件编译成 class 文件的时候注解被遗弃;

RetentionPolicy.CLASS 表示注解被保留到 class 文件,当 JVM 加载 class 文件的时候被遗弃。

RetentionPolicy.RUNTIME 表示 JVM 加载 class 文件后依然存在,在运行过程中可以在任意时间被调用。

RetentionPolicy.RUNTIME 比较容易理解,它就是用在为反射而生的。另外两个都可以用于编译时注解。

@Target有九种枚举类型:

TYPE 作用于接口、类、枚举。

FIELD 作用于字段、变量。

METHOD 作用于方法。

PARAMETER 作用于形参。

CONSTRUCTOR 作用于构造方法。

LOCAL_VARIABLE 作用于局部变量。

ANNOTATION_TYPE 作用于注解。

PACKAGE 作用于包名。

TYPE_PARAMETER 作用于类型参数(如泛型、类型转换)。

TYPE_USE 作用于类型使用时。

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface TestField {
  String value();
}
复制代码

以上代码就通过了元注解实现了注解, TestField 能注解在变量上,并且只保留在 class 文件,运行后这个注解就会消失。使用如下:

@TestField("hello")
String value;
复制代码

2. 注解处理器模块

编译时注解往简单的说就是在 Java 代码编译成 class 字节码的过程中执行注解处理器并生成你需要的 Java 文件。

所以第一个问题就是如何在编译时执行?答案是继承 AbstractProcessor 就可以了,编译器会自动寻找继承 AbstractProcessor 的类,并调用它的 process 方法,一般来说我们会重写以下几个方法:

public class TestProcessor extends AbstractProcessor {
  @Override
  public Set<String> getSupportedAnnotationTypes(){
    Set<String> annotationTypes = new LinkedHashSet<String>();
    annotationTypes.add(TestField.class.getCanonicalName());
    return annotationTypes;
  }
  @Override
  public SourceVersion getSupportedSourceVersion(){
    return SourceVersion.latestSupported();
  }
  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){}
}
复制代码

getSupportedAnnotationTypes 方法添加我们需要解析的注解, getSupportedSourceVersion 方法返回最新的版本支持即可。主要的代码处理在 process 方法,继续实现上一小节的注解,我们可以把 TestField 注解的值赋给 value 变量, 这个过程中我们需要先检测有此注解的变量并保存到一个集合中,然后再拿集合内的信息去生成 Java 代码 。先来看下搜集信息的并保存到集合的代码:

private Map<String, ClassInfo> classInfos = new HashMap<>();

public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){
  classInfos.clear();
  for(TypeElement annotation: annotations){
    // 获取一个类中所有节点,这里可以是域、方法、类节点等等。
    Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(annotation);
    for(Element element : elements) {
      // 找到变量节点
      if(element instanceof VariableElement){
        // 获取变量所在类的全限定类名
        String qualifiedClassName = ((TypeElement)element.getEnclosingElement()).getQualifiedName().toString();
        ClassInfo classInfo = classInfos.get(qualifiedClassName);
        if(classInfo == null){
          classInfo = new ClassInfo();
        }
        classInfo.qualifiedClassName = qualifiedClassName;
        classInfo.typeElement = (TypeElement)element.getEnclosingElement();
        classInfo.variableElements.add(element);
        // 以全限定类名作为key可保证唯一性
        classInfos.put(qualifiedClassName, classInfo);
      }else {
        // 在本例中TestField只修饰所有域,因此如果出现不是「变量节点」的话就抛异常吧
      }
    }
  }
}

/**
* 保存一个类文件中我们所需要的信息
*/
public static final class ClassInfo {
  String qualifiedClassName; //全限定类名
  TypeElement typeElement; // 类节点
  List<VariableElement> variableElements = new ArrayList<>(); // 一个类中所有有该注解的变量节点
  
  // 获取非限定类名
  public String getClassName(){
    if(qualifiedClassName == null){
      return null;
    }
    return qualifiedClassName.substring(qualifiedClassName.lastIndexof(".") + 1, qualifiedClassName.length());
  }
}
复制代码

简单介绍下 Element 的子类:

VariableElement
ExecutableElement
TypeElement
PackageElement

上面的代码注释也比较清晰了, 就是每个有指定注解的类会被遍历到,然后把它里面所有 VariableElement 保存起来

第二个步骤就是把已经收集的信息生成对应的 Java 文件。

private static final String SUFFIX = "$ITest";
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){
  // 收集信息的代码...
  // 开始生成Java文件
  for(ClassInfo classInfo : classInfos.values()){
    try{
      // 创建java文件对象,全限定类名+指定后缀
      JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(classInfo.qualifiedClassName + SUFFIX. classInfo.typeElement);
      // 开始写入代码
      Writer writer = sourceFile.openWriter();
      writer.write(generateCode(classInfo));
      writer.flush();
      writer.close();
    }catch (IOException e){
      e.printStackTrace();
    }
  }
  return true;
}

public String generateCode(ClassInfo classInfo){
  StringBuilder builder = new StringBuilder();
  // 获取包名
  String packageName = processEnv.getElementUtils().getPackageOf(classInfo.typeElement).getQualifiedName().toString();
  builder.append("package " + packageName + ";/n")
  		 .append("import com.test.api.*;/n")
  		 .append("public class " + classInfo.getClassName() + SUFFIX + " implements ")
  		 .append("ITest<" + classInfo.qualifiedClassName + ">{/n")
  		 .append("	public void inject(" + classInfo.qualifiedClassName + " host){/n")
  		 .append(generateInject(classInfo))
  		 .append("/n  }")
  		 .append("/n}");
}

public String generateInject(ClassInfo classInfo){
  StringBuilder builder = new StringBuilder();
  for(VariableElement element : classInfo.variableElements){
    TestField test = element.getAnnotation(TestField.class);
    if(test != null){
      // 获取注解上的值
      int value = test.value();
      String variableName = element.getSimpleName().toString();
      // 给变量赋值
      builder.append("		host." + elementName + "=" + value + "/n");
    }
  }
  return builder.toString();
}
复制代码

这段代码创建了 Java 文件,然后在里面使用收集到的信息拼接字符串,非常容易理解。如果复杂一些的项目可以考虑使用 javapoet 库。

最后在需要在 src/main/ 下新建 resources 文件夹,再新建 META-INF.services 文件夹,在此文件夹内新建 javax.annotation.processing.Processor 文件,在文件内写入你的注解器的全限定类名,如:

com.test.processor.TestProcessor
复制代码

这样注解处理器就注册成功了,在编译器会自动执行到这个注解。不过还有更简单的一种方式,在build.gradle下加入以下依赖:

compile 'com.google.auto.service:auto-service:1.0-rc4'
复制代码

然后在自定义注解处理器的类上加上如下代码:

@AutoService(Processor.class)
public class TestProcessor extends AbstractProcessor {
  
}
复制代码

这样就会自动注册 TestProcessor 注解类。

3. Api模块

Api 模块就是提供给开发者调用之前注解处理器生成的代码。

在这个示例中我们提供一个接口,所有生成的 Java 类都会实现这个接口,方便统一调用。就是上面生成代码中已经出现的 ITest

public interface ITest<T> {
  void inject(T obj);
}
复制代码

最终写一个 app 可调用的方法:

public class TestApi {
  public static void inject(Object obj){
    Class<?> clazz = obj.getClass();
    String proxyName = clazz.getName() + "$ITest";
    // 省略 try catch
    Class<?> proxyClazz = Class.forName(proxyName);
    ITest test = (ITest) proxyClazz.newInstance();
    test.inject(obj);
  }
}
复制代码

app 内使用如下:

public class MainActivity extends AppCompatActivity {
  @TestField(2)
  public int value;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //.....
    TestApi.inject(this);
  }
}
复制代码

使用也很简单,就是在变量上使用了 @TestField 注解,然后调用 TestApi.inject(this) 去调用已经生成的 Java 类。最后看下编译时我们生成的 Java 文件是怎样的,其位置位于 build/generated/source/apt/

package com.test.project;
import com.test.api.*;
public class MainActivity$ITest implements ITest<com.test.project.MainActivity>{
  public void inject(com.test.project.MainActivity host){
    host.value = 2;
  }
}
复制代码

所以在调用 inject 方法之后, MainActivity 中的 value 就被赋值为 2 了。

看起来编译时注解能做很多事情,而且把操作放在编译期就不会拖慢程序运行时的速度,所以很多框架采取这种方式代替注解反射。不过同样的,注解处理器生成的类也会增大 app 的体积,这可能是编译时注解的一个不足。

二、EventBus源码分析

在 EventBus 3.0 之后也加入了编译时注解, 以下内容主要讲解注解处理器是如何生成 Index 类,并通过使用编译时生成的 Index 类来订阅、分发事件的整个流程。

1. MyEventBusIndex

要想使用编译时注解,需要在 build.gradle 内添加如下脚本:

android {
  defaultConfig {
    javaCompileOptions {
       annotationProcessorOptions {
         arguments = [ eventBusIndex : 'com.example.myapplication.MyEventBusIndex' ]
       }
    }
  }
}
dependencies {
  implementation 'org.greenrobot:eventbus:3.1.1'
  annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.1.1'
}
复制代码

annotationProcessor 依赖注解处理器没什么问题,那么 arguments 这个参数又是什么用处呢?我们带着问题去看下注解处理器的代码:

public class EventBusAnnotationProcessor extends AbstractProcessor {
    public static final String OPTION_EVENT_BUS_INDEX = "eventBusIndex";
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
       // ...... 
       // 这里拿到了gradle内的配置,也就是 com.example.myapplication.MyEventBusIndex
       String index = processingEnv.getOptions().get(OPTION_EVENT_BUS_INDEX);
       // 收集节点信息并保存
       collectSubscribers(annotations, env, messager);
       // 生成 Java 文件
       createInfoIndexFile(index);
    }
}
复制代码

套路和前一节讲的一样。

先是收集信息,由于 EventBusSubscribe 注解只作用在方法上,因此只要使用一个集合,其 key 为 全限定类名 或 TypeElement (事实上 Eventbus 是以 TypeElementkey ), valueExecutableElement 方法节点列表。

然后是根据收集到的信息生成 Java 文件,这个文件的路径是 com.example.myapplication.MyEventBusIndex 。这里就不再详细展开,想详细了解可以去官方文档里看下源码,理解了编译时注解基础后这些代码是比较容易理解的。假设我们在 MainActivity 中某个方法上做了如下注解:

@Subscribe
public void testMethod(TestEvent event){
}
复制代码

那么在 build/generated/source/apt/ 生成的 MyEventBusIndex 类就是如下:

public class MyEventBusIndex implements SubscriberInfoIndex {
    private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;
    static {
        SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>();
        putIndex(new SimpleSubscriberInfo(MainActivity.class, true, new SubscriberMethodInfo[] {
            new SubscriberMethodInfo("testMethod", TestEvent.class),
        }));

    }
    private static void putIndex(SubscriberInfo info) {
        SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info);
    }
    @Override
    public SubscriberInfo getSubscriberInfo(Class<?> subscriberClass) {
        SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);
        if (info != null) {
            return info;
        } else {
            return null;
        }
    }
}
复制代码

这里简单解释下几个类的作用。

  • MyEventBusIndex:实现了 SubscriberInfoIndex 接口,主要有两个逻辑,一是保存以订阅者 class 对象为 keySubscriberInfovalue 的集合;二是重写 getSubscriberInfo 方法,将指定的 class 对象的 SubscriberInfo 返回出去。
  • SubscriberInfo:一个接口,能提供订阅者 class 对象、订阅者被注解修饰的方法、订阅者父类的 SubscriberInfo
  • SimpleSubscriberInfo :继承了 AbstractSubscriberInfoAbstractSubscriberInfo 则实现了 SubscriberInfoSimpleSubscriberInfoEventBus 默认唯一一个实现 SubscriberInfo 的类,可想而知它提供了让你自己去编写注解处理器和自定义 SubscriberInfo 的可能性。从它的实现上能看出 SimpleSubscriberInfo 内保存了订阅者 class 对象、订阅者方法信息等。
  • SubscriberMethodInfo:是 SimpleSubscriberInfo 的一个成员变量,编译时会把 @Subscribe 注解所修饰的方法名、形参类型、 threadMode、priority、sticky 都解析出来,并保存到 SubscriberMethodInfo 中。

最后需要调用提供的 api ,将 MyEventBusIndex 添加到其中:

EventBus.builder().addIndex(new MyEventBusIndex()).installDefaultEventBus();
复制代码

2. 订阅分发流程

接下来看下 registerpost 流程,主要看使用 MyEventBusIndex 类的逻辑,本文不涉及注解反射的逻辑。

订阅流程

public void register(Object subscriber) {
    // 获取订阅者的 class 对象
    Class<?> subscriberClass = subscriber.getClass();
    // 通过subscriberMethodFinder解析出这个订阅者被订阅的所有方法
    List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
    synchronized (this) {
        for (SubscriberMethod subscriberMethod : subscriberMethods) {
            // 把订阅信息保存到集合中
            subscribe(subscriber, subscriberMethod);
        }
    }
}
复制代码

接下来走到 subscriberMethodFinder 里是如何解析出被 @Subscribe 注解的方法:

List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
	// 先从缓存中取,因此即使是注解反射也不会耗费多少时间
    List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
    if (subscriberMethods != null) {
        return subscriberMethods;
    }
    if (ignoreGeneratedIndex) {
    	// 通过注解反射获取被订阅的方法
        subscriberMethods = findUsingReflection(subscriberClass);
    } else {
    	// 通过编译时注解生成的 MyEventBusIndex 获取被订阅的方法
        subscriberMethods = findUsingInfo(subscriberClass);
    }
    // ......
    METHOD_CACHE.put(subscriberClass, subscriberMethods);
    return subscriberMethods;
}
复制代码

其实即使使用反射也并不会耗费多少时间,因为 EventBus 会只会在第一次使用时反射,之后都使用缓存。跳过反射部分,直接看 findUsingInfo 方法:

private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
    FindState findState = prepareFindState();
    findState.initForSubscriber(subscriberClass);
    while (findState.clazz != null) {
        findState.subscriberInfo = getSubscriberInfo(findState);
        // ......
    }
    // ......
}
复制代码

FindState 类可以理解为保存了 SubscriberInfo 、SubscriberMethod、class对象 等信息,在之后会使用到。关键在于 getSubscriberInfo 方法。

private SubscriberInfo getSubscriberInfo(FindState findState) {
    // ......
    if (subscriberInfoIndexes != null) {
        for (SubscriberInfoIndex index : subscriberInfoIndexes) {
            SubscriberInfo info = index.getSubscriberInfo(findState.clazz);
            if (info != null) {
                return info;
            }
        }
    }
    return null;
}
复制代码

subscriberInfoIndexes 是一个 List 数据结构,我们之前调用 EventBus.builder().addIndex(new MyEventBusIndex()) 其实就是将 MyEventBusIndex 添加到 subscriberInfoIndexes 中,这个时候我们就可以取出订阅者 class 对象对应的 SubscriberInfo ,还记得它保存了订阅者被注解 @Subscribe 所修饰的方法。最终会通过 SubscriberInfo 返回对应的方法列表,我们再回到 register 方法,在拿到订阅方法列表后,调用 subscribe 方法:

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
    Class<?> eventType = subscriberMethod.eventType;
    Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
    CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
    if (subscriptions == null) {
        subscriptions = new CopyOnWriteArrayList<>();
        subscriptionsByEventType.put(eventType, subscriptions);
    } 
    // ...
}
复制代码

eventType 是被订阅方法的参数的 class 对象, EventBus 事件分发就是根据参数分发到对应的方法上的,因此要保存以 eventTypekeysubscriptionsByEventType 集合,在之后的分发流程中会使用到。

分发流程

public void post(Object event) {
	// 某个线程都会有自己的PostingThreadState
    PostingThreadState postingState = currentPostingThreadState.get();
    List<Object> eventQueue = postingState.eventQueue;
    // 保证消息能按添加顺序分发
    eventQueue.add(event);
    // isPosting标志位防止多次分发
    if (!postingState.isPosting) {
        postingState.isMainThread = isMainThread();
        postingState.isPosting = true;
        try {
            while (!eventQueue.isEmpty()) {
                postSingleEvent(eventQueue.remove(0), postingState);
            }
        } finally {
            postingState.isPosting = false;
            postingState.isMainThread = false;
        }
    }
}
复制代码

这段方法核心就是从消息队列中取 event 消息然后调用 postSingleEvent 方法, postSingleEvent 方法内部主要是对父类对象 eventType 的检查,默认是开启父类检查的,如果想要加快事件分发的速度而且不需要分发给父类,可以考虑把标志位改为不检查父类,接着会调用 postToSubscription 方法,

private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
    switch (subscription.subscriberMethod.threadMode) {
        case POSTING:
            invokeSubscriber(subscription, event);
            break;
        case MAIN:
            if (isMainThread) {
                invokeSubscriber(subscription, event);
            } else {
                mainThreadPoster.enqueue(subscription, event);
            }
            break;
        case MAIN_ORDERED:
            if (mainThreadPoster != null) {
                mainThreadPoster.enqueue(subscription, event);
            } else {
                // temporary: technically not correct as poster not decoupled from subscriber
                invokeSubscriber(subscription, event);
            }
            break;
        case BACKGROUND:
            if (isMainThread) {
                backgroundPoster.enqueue(subscription, event);
            } else {
                invokeSubscriber(subscription, event);
            }
            break;
        case ASYNC:
            asyncPoster.enqueue(subscription, event);
            break;
    }
}
复制代码

这里有 5 种 threadMode

  • POSTING:分发事件在哪个线程,订阅者的方法就会在哪个线程直接被调用。
  • MAIN:无论分发事件在主线程还是子线程,方法会在主线程中被调用。
  • MAIN_ORDERED:方法会在主线程中被调用,而且方法被调用的顺序和事件分发的顺序一致。
  • BACKGROUND:如果事件分发在主线程,方法调用则会在子线程,如果事件分发已经在子线程了,那么直接在这个线程内调用方法。
  • ASYNC:方法调用一定会在另一个线程中。

默认是 POSTING 策略,我们看下 invokeSubscriber 方法做了什么:

void invokeSubscriber(Subscription subscription, Object event) {
    try {
        subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
    } catch (InvocationTargetException e) {
        handleSubscriberException(subscription, event, e.getCause());
    } catch (IllegalAccessException e) {
        throw new IllegalStateException("Unexpected exception", e);
    }
}
复制代码

这里再熟悉不过了,通过 method.invoke 反射调用到真实的方法,这里就有疑问了,你这还是用到了反射啊?其实很多框架是避免不了反射的,只是尽量的少用反射能节省不少时间。

3. 小结

以上就是编译时注解生成 MyEventBusIndex , 然后 EventBus 订阅分发的整个流程。下面用两张图总结下订阅、分发两个流程。

EventBus 类图:

EventBus 3.0 从编译时注解分析源码

EventBus 时序图(注解反射):

EventBus 3.0 从编译时注解分析源码

三、总结

本文主要从编译时注解为核心,讲述了编译时注解的基础以及如何编写一个简单的注解处理器,这对于阅读使用到编译时注解的开源库源码有很大的帮助。接着从 EventBus 3.0 的注解处理器开始分析,在了解了编译时注解的基础后能较容易的理解 MyEventBusIndex 类是如何生成的。然后继续跟进分析了 EventBus.register 订阅流程和 EventBus.post 事件分发的流程。

参考资料

Android 如何编写基于编译时注解的项目

EventBus官方文档

原文  https://juejin.im/post/5d4149adf265da03f12e36f6
正文到此结束
Loading...