EventBus源码赏析五 —— 注解处理器

1,272 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第24天,点击查看活动详情

EventBus从3.0开始引入了编译时注解,利用EventBusAnnotationProcessor类在编译期间收集@Subscribe所包含的信息。EventBusAnnotationProcessorAbstractProcessor的子类,关于注解处理器的使用这里不做过多的介绍,主要看看EventBus是如何收集订阅方法的。

主要流程在process()方法中,分为以下四步。

public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
    //1.预处理
    //2.收集@Subscriber注解的方法
    collectSubscribers()
    //3.检查
    checkForSubscribersToSkip()
    //4.生成索引文件
    createInfoIndexFile()
}

预处理

预处理主要是对配置进行检查,以及初始化一些信息。

String index = processingEnv.getOptions().get("eventBusIndex");
if (index == null) {
    messager.printMessage(Diagnostic.Kind.ERROR, "No option eventBusIndex passed to annotation processor");
    return false;
}
int lastPeriod = index.lastIndexOf('.');
String indexPackage = lastPeriod != -1 ? index.substring(0, lastPeriod) : null;
if (env.processingOver()) {
    if (!annotations.isEmpty()) {
        messager.printMessage(Diagnostic.Kind.ERROR,
                "Unexpected processing state: annotations still available after processing over");
        return false;
    }
}
if (annotations.isEmpty()) {
    return false;
}

首先会检查我们对索引类的配置,如果没有配置,会直接异常返回,否则根据配置截取出索引类的包名,用于后面的检测。然后还会判断RoundEnvironment的状态以及注解信息是否存在。

收集@Subscriber注解的方法

接下来会根据annotations获取@Subscriber注解的方法

private void collectSubscribers(Set<? extends TypeElement> annotations, RoundEnvironment env, Messager messager) {
    for (TypeElement annotation : annotations) {
        Set<? extends Element> elements = env.getElementsAnnotatedWith(annotation);
        for (Element element : elements) {
            if (element instanceof ExecutableElement) {
                ExecutableElement method = (ExecutableElement) element;
                if (checkHasNoErrors(method, messager)) {
                    TypeElement classElement = (TypeElement) method.getEnclosingElement();
                    methodsByClass.putElement(classElement, method);
                }
            } else {
                messager.printMessage(Diagnostic.Kind.ERROR, "@Subscribe is only valid for methods", element);
            }
        }
    }
}

首先通过getElementsAnnotatedWith()方法搜索整个Module中所有使用了@Subscribe注解的元素,这里的元素Element很能是方法,字段,类等。常用的如下:

  1. ExecutableElement 表示类或者接口中的方法,构造函数或者初始化器。
  2. PackageElement 表示包程序元素
  3. TypeElement 表示一个类或者接口元素
  4. TypeParameterElement 表示类,接口,方法的泛型类型例如T。
  5. VariableElement 表示字段,枚举常量,方法或者构造函数参数,局部变量,资源变量或者异常参数。

所以对于每一个元素,需要先判断是不是ExecutableElement,对于方法还需要进一步判断是否合法。

private boolean checkHasNoErrors(ExecutableElement element, Messager messager) {
    if (element.getModifiers().contains(Modifier.STATIC)) {
        messager.printMessage(Diagnostic.Kind.ERROR"Subscriber method must not be static", element);
        return false;
    }
    if (!element.getModifiers().contains(Modifier.PUBLIC)) {
        messager.printMessage(Diagnostic.Kind.ERROR"Subscriber method must be public", element);
        return false;
    }
    List<? extends VariableElement> parameters = ((ExecutableElement) element).getParameters();
    if (parameters.size() != 1) {
        messager.printMessage(Diagnostic.Kind.ERROR"Subscriber method must have exactly 1 parameter", element);
        return false;
    }
    return true;
}

这里的检测与使用反射时筛选方法差不多,都需要满足以下条件:

  1. 方法不能是静态的(STATIC)
  2. 方法必须是公开的(PUBLIC)
  3. 方法的参数必须是能有一个

对于合法的方法需要收集起来,存储在methodsByClass中,methodsByClassListMap<TypeElement, ExecutableElement>类型,是以TypeElement为单位的,所以需要先通过method.getEnclosingElement()获取TypeElement,然后保存进methodsByClass

检测与筛选

经过上一步收集完订阅方法之后,还需要对这些方法以及方法所在的类等信息做一些检查

订阅者检查

private void checkForSubscribersToSkip(Messager messager, String myPackage) {
    for (TypeElement skipCandidate : methodsByClass.keySet()) {
        TypeElement subscriberClass = skipCandidate;
        while (subscriberClass != null) {
	    //检测订阅者
            if (!isVisible(myPackage, subscriberClass)) {
                boolean added = classesToSkip.add(skipCandidate);
                //省略部分无关代码
                break;
            }
	    //检测订阅方法
            //.....
            subscriberClass = getSuperclass(subscriberClass);
        }
    }
}

首先根据isVisible(myPackage, subscriberClass)判断可见性。

private boolean isVisible(String myPackage, TypeElement typeElement) {
    Set<Modifier> modifiers = typeElement.getModifiers();
    boolean visible;
    if (modifiers.contains(Modifier.PUBLIC)) {
        visible = true;
    } else if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.PROTECTED)) {
        visible = false;
    } else {
        String subscriberPackage = getPackageElement(typeElement).getQualifiedName().toString();
        if (myPackage == null) {
            visible = subscriberPackage.length() == 0;
        } else {
            visible = myPackage.equals(subscriberPackage);
        }
    }
    return visible;
}
  • 访问修饰符是public则返回true
  • 访问修饰符是private或者protected则返回false
  • 访问修饰符是默认且订阅方法和索引类在同一个包则返回true,否则返回false

只有公开类和修饰符是默认且订阅方法和索引类同包的类才算合法,对于不合法的类会收集在classesToSkip中,合法的则检查其订阅方法的参数(包括当前类和其父类)

订阅方法参数检查

//上面checkForSubscribersToSkip()方法中检测订阅方法处省略部分
List<ExecutableElement> methods = methodsByClass.get(subscriberClass);
if (methods != null) {
    for (ExecutableElement method : methods) {
        String skipReason = null;
        VariableElement param = method.getParameters().get(0);
        TypeMirror typeMirror = getParamTypeMirror(param, messager);
        if (!(typeMirror instanceof DeclaredType) ||
                !(((DeclaredType) typeMirror).asElement() instanceof TypeElement)) {
            skipReason = "event type cannot be processed";
        }
        if (skipReason == null) {
            TypeElement eventTypeElement = (TypeElement) ((DeclaredType) typeMirror).asElement();
            if (!isVisible(myPackage, eventTypeElement)) {
                skipReason = "event type is not public";
            }
        }
        if (skipReason != null) {
            boolean added = classesToSkip.add(skipCandidate);
             //省略部分无关代码
            break;
        }
    }
}

首先遍历之前收集的当前订阅者的注解方法,获取方法的第一个参数,由于在收集的时候限制了该方法只能有一个参数,所以这个参数也就是处理的事件对象。

然后获取该参数的TypeMirror,TypeMirror是元素的类型信息,通过它判断该参数的类型是不是类或者接口,如果不是就跳过该类,否则判断参数的可见性,判断方法与上面订阅者判断一致,都是根据isVisible(myPackage, subscriberClass)判断。

最后将不合法的订阅者加入classesToSkip中。

遍历父类

while (subscriberClass != null) {
	//省略相关处理代码
    subscriberClass = getSuperclass(subscriberClass);
}

当前类处理完了之后,还需要处理其父类,逻辑都一致。获取父类的方法比较简单

private TypeElement getSuperclass(TypeElement type) {
    if (type.getSuperclass().getKind() == TypeKind.DECLARED) {
        TypeElement superclass = (TypeElement) processingEnv.getTypeUtils().asElement(type.getSuperclass());
        String name = superclass.getQualifiedName().toString();
        if (name.startsWith("java.") || name.startsWith("javax.") || name.startsWith("android.")) {
            return null;
        } else {
            return superclass;
        }
    } else {
        return null;
    }
}

这里对父类的获取做了一些限制,减少了不必要的开销,父类必须是TypeKind.DECLARED(类或者接口)类型,并且不是java/javax/android 开头的系统类。

输出索引类

将订阅者和其订阅方法都收集过滤了之后,就可以开始生成文件了

private void createInfoIndexFile(String index) {
    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());
        //省略前面一些固定的写逻辑
        writeIndexLines(writer, myPackage);
        //省略后面一些固定的写逻辑
    } catch (IOException e) {
        throw new RuntimeException("Could not write source for " + index, e);
    } finally {
        //省略处理
    }
}

写文件的过程很简单,EventBus没有使用JavaPoet,而是使用了Filer配合BufferedWriter直接硬编码write。 首先是一些导包,变量定义等模板代码,然后是写注解生成的索引信息,最后是一些获取订阅信息的方法。

private void writeIndexLines(BufferedWriter writer, String myPackage) throws IOException {
    for (TypeElement subscriberTypeElement : methodsByClass.keySet()) {
        if (classesToSkip.contains(subscriberTypeElement)) {
            continue;
        }
        String subscriberClass = getClassString(subscriberTypeElement, myPackage);
        if (isVisible(myPackage, subscriberTypeElement)) {
            writeLine(writer, 2"putIndex(new SimpleSubscriberInfo(" + subscriberClass + ".class,""true,""new SubscriberMethodInfo[] {");
            List<ExecutableElement> methods = methodsByClass.get(subscriberTypeElement);
            writeCreateSubscriberMethods(writer, methods, "new SubscriberMethodInfo", myPackage);
            writer.write("        }));\n\n");
        } else {
            writer.write("        // Subscriber not visible to index: " + subscriberClass + "\n");
        }
    }
}

先遍历之前收集的订阅类,跳过不合法的(存入classesToSkip的),然后判断该类是否能访问事件类,能的话就遍历写入文件

private void writeCreateSubscriberMethods(BufferedWriter writer, List<ExecutableElement> methods,
                                              String callPrefix, String myPackage) throws IOException {
    for (ExecutableElement method : methods) {
    }    
}

写入流程都是按照模板写入,最后的订阅方法结果样式如下:

putIndex(new SimpleSubscriberInfo(com.EventBusActivity.classtruenew SubscriberMethodInfo[] {        
    new SubscriberMethodInfo("parentMessage", com.IntEvent.class, ThreadMode.BACKGROUND),
}));

最后生成的整个文件如下:

//省略导包
public class MyEventBusIndex implements SubscriberInfoIndex {
    private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;
    static {
        SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>();
        putIndex(new SimpleSubscriberInfo(com.EventBusOtherActivity.classtruenew SubscriberMethodInfo("msg",com.IntEvent.classThreadMode.MAIN1true),
        }));
    }
    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;
        }
    }
}