Android-EventBus修改纪实(四)-注解处理器

Android-EventBus修改纪实(四)

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 6 天,点击查看活动详情


前言

本文是 EventBus 修改纪实的第四篇,笔者在写第一篇文章时只是想记录下修改 EventBus 的过程,分享解决问题和查看源码的思路,没想到不知不觉会写这么多,“天下无不散之筵席”,本文将分析 EventBus 注解处理器的流程,也是 EventBus 修改纪实的最后一篇文章。

纪实

EventBus 的注解处理器程序只有一个类 EventBusAnnotationProcessor

@SupportedAnnotationTypes("com.yarward.org.greenrobot.eventbus.Subscribe")
@SupportedOptions(value = {"eventBusIndex", "verbose"})
@IncrementalAnnotationProcessor(AGGREGATING)
public class EventBusAnnotationProcessor extends AbstractProcessor {
    // ......
}
复制代码

EventBusAnnotationProcessor 上有三个注解,下面一一介绍:

  • @SupportedAnnotationTypes 注解标识该注解处理器只处理 @Subscribe 注解,
  • @SupportedOptions 注解标识改注解处理器可以有两个参数,eventBusIndex 标识要生成 Index 类的全量限定名称,verbose 主要用于日志输出
  • @IncrementalAnnotationProcessor 注解方便构建增量注解处理器

process

接下来分析处理流程,处理流程主要在 process 方法中:

public static final String OPTION_EVENT_BUS_INDEX = "eventBusIndex";
public static final String OPTION_VERBOSE = "verbose";

// annotations 是 `@Subscribe` 注解集合
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
    // 获取日志输出器
    Messager messager = processingEnv.getMessager();
    try {
        // 获取 `eventBusIndex` 参数
        String index = processingEnv.getOptions().get(OPTION_EVENT_BUS_INDEX);
        if (index == null) {
            messager.printMessage(Diagnostic.Kind.ERROR, "No option " + OPTION_EVENT_BUS_INDEX +
                                  " passed to annotation processor");
            return false;
        }
        // 获取 `verbose` 参数
        verbose = Boolean.parseBoolean(processingEnv.getOptions().get(OPTION_VERBOSE));
        int lastPeriod = index.lastIndexOf('.');
        // 获取 eventBusIndex 的包名
        String indexPackage = lastPeriod != -1 ? index.substring(0, lastPeriod) : null;

        round++;
        if (verbose) {
            messager.printMessage(Diagnostic.Kind.NOTE, "Processing round " + round + ", new annotations: " +
                                  !annotations.isEmpty() + ", processingOver: " + env.processingOver());
        }
        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;
        }

        if (writerRoundDone) {
            messager.printMessage(Diagnostic.Kind.ERROR,
                                  "Unexpected processing state: annotations still available after writing.");
        }
        // 收集订阅者和订阅方法
        collectSubscribers(annotations, env, messager);
        // 检查是否可以为订阅者生成 Index
        checkForSubscribersToSkip(messager, indexPackage);

        if (!methodsByClass.isEmpty()) {
            // 生成 Index 文件
            createInfoIndexFile(index);
        } else {
            messager.printMessage(Diagnostic.Kind.WARNING, "No @Subscribe annotations found");
        }
        writerRoundDone = true;
    } catch (RuntimeException e) {
        // IntelliJ does not handle exceptions nicely, so log and print a message
        e.printStackTrace();
        messager.printMessage(Diagnostic.Kind.ERROR, "Unexpected error in EventBusAnnotationProcessor: " + e);
    }
    return true;
}
复制代码

collectSubscribers

我们首先分析是如何收集订阅者和订阅方法的:

// 自定义集合,实际上是 Map<TypeElement, List<ExecutableElement>>
private final ListMap<TypeElement, ExecutableElement> methodsByClass = new ListMap<>();

// annotations 是 `@Subscribe ` 注解集合
private void collectSubscribers(Set<? extends TypeElement> annotations, RoundEnvironment env, Messager messager) {
    // 遍历 `@Subscribe` 注解集合
    for (TypeElement annotation : annotations) {
        // 获取有 `@Subscribe` 注解的元素集合
        Set<? extends Element> elements = env.getElementsAnnotatedWith(annotation);
        for (Element element : elements) {
            // 判断是不是可执行的元素,一般表示是 Java 方法
            if (element instanceof ExecutableElement) {
                ExecutableElement method = (ExecutableElement) element;
                // 检查是否是订阅方法
                if (checkHasNoErrors(method, messager)) {
                    TypeElement classElement = (TypeElement) method.getEnclosingElement();
                    // 添加进 Map 集合
                    methodsByClass.putElement(classElement, method);
                }
            } else {
                messager.printMessage(Diagnostic.Kind.ERROR, "@Subscribe is only valid for methods", element);
            }
        }
    }
}
复制代码

首先遍历 @Subscribe 注解集合,通过 getElementsAnnotatedWith 获取有 @Subscribe 注解的元素集合,然后遍历元素集合,在遍历元素集合时判断元素是否是 ExecutableElement,即当前元素是不是方法,如何是方法再通过 checkHasNoErrors 检查此方法是否符合订阅方法的规范:

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;
}
复制代码

检查方法比较简单,需要满足以下条件:

  • 必须不能是静态方法
  • 必须是公开方法
  • 方法有且仅有一个入参

注意这里没有判断是否是桥接方法和合成方法,因为注解处理器处于编译期,桥接方法和合成方法应该经过 javac 编译后才会有。

最后把符合订阅方法规范的方法收集起来,存储在 methodsByClass 中,methodsByClass 是 greenrobot 自定义的,其实际类型是 Map<TypeElement, List<ExecutableElement>>,以订阅者为 Key,订阅者中的订阅方法集合为 Value。

checkForSubscribersToSkip

private void checkForSubscribersToSkip(Messager messager, String myPackage) {
    // 遍历 订阅者
    for (TypeElement skipCandidate : methodsByClass.keySet()) {
        TypeElement subscriberClass = skipCandidate;
        // 循环判断订阅者及其父类
        while (subscriberClass != null) {
            // 判断订阅者是否可见
            if (!isVisible(myPackage, subscriberClass)) {
                // 若不可见,则添加进跳过订阅者集合,退出 while 循环
                boolean added = classesToSkip.add(skipCandidate);
                if (added) {
                    String msg;
                    if (subscriberClass.equals(skipCandidate)) {
                        msg = "Falling back to reflection because class is not public";
                    } else {
                        msg = "Falling back to reflection because " + skipCandidate +
                            " has a non-public super class";
                    }
                    messager.printMessage(Diagnostic.Kind.NOTE, msg, subscriberClass);
                }
                break;
            }
            
            // 获取订阅者中的订阅方法集合
            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();
                        // 判断 Event 是否可见
                        if (!isVisible(myPackage, eventTypeElement)) {
                            skipReason = "event type is not public";
                        }
                    }
                    if (skipReason != null) {
                        // 添加进跳过订阅者集合,退出 for 循环
                        boolean added = classesToSkip.add(skipCandidate);
                        if (added) {
                            String msg = "Falling back to reflection because " + skipReason;
                            if (!subscriberClass.equals(skipCandidate)) {
                                msg += " (found in super class for " + skipCandidate + ")";
                            }
                            messager.printMessage(Diagnostic.Kind.NOTE, msg, param);
                        }
                        break;
                    }
                }
            }
            
            // 移动至父类
            subscriberClass = getSuperclass(subscriberClass);
        }
    }
}
复制代码

首先调用 isVisible 方法判断订阅者是否可见:

private boolean isVisible(String myPackage, TypeElement typeElement) {
    // 获取描述符集合
    Set<Modifier> modifiers = typeElement.getModifiers();
    boolean visible;
    
    // 判断是否是 PUBLIC
    if (modifiers.contains(Modifier.PUBLIC)) {
        visible = true;
        // 判断是否是 PRIVATE 或者 PROTECTED
    } else if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.PROTECTED)) {
        visible = false;
    } else {
        // 其他情况
        // 获取元素的包名
        String subscriberPackage = getPackageElement(typeElement).getQualifiedName().toString();
        // 若 Index 类包名为空,则根据元素包名的长度判断,否则比较 Index 类和元素是否在同一个包下
        if (myPackage == null) {
            visible = subscriberPackage.length() == 0;
        } else {
            visible = myPackage.equals(subscriberPackage);
        }
    }
    return visible;
}
复制代码

如果订阅者符合以下条件即认为可见:

  • 订阅者含有 public 修饰符
  • Index 类包名为空时,订阅者包名长度为 0,
  • Index 类包名不为空,订阅者和 Index 类同属一个包下

如果订阅者不符合可见性条件,则添加进 classesToSkip 集合,后续生成 Index 类时会忽略此这些订阅者。

如果订阅者符合可见性条件,接下来判断订阅方法是否符合规范:

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();
        // 判断 Event 是否可见
        if (!isVisible(myPackage, eventTypeElement)) {
            skipReason = "event type is not public";
        }
    }
    if (skipReason != null) {
        // 添加进跳过订阅者集合,退出 for 循环
        boolean added = classesToSkip.add(skipCandidate);
        if (added) {
            String msg = "Falling back to reflection because " + skipReason;
            if (!subscriberClass.equals(skipCandidate)) {
                msg += " (found in super class for " + skipCandidate + ")";
            }
            messager.printMessage(Diagnostic.Kind.NOTE, msg, param);
        }
        break;
    }
}
复制代码

遍历订阅方法,判断订阅方法的入参如果不是声明类型,即不是基本类型;或者入参是声明类型但不是类或接口,则将订阅者添加进跳过订阅者集合并退出 for 循环,否则再判断入参是否可见,若是不可见,则将订阅者添加进跳过订阅者集合并退出 for 循环。

最后获取当前订阅者的父类,循环判断父类是否也符合以上规范,若是父类不符合以上规范,则将当前订阅者添加进跳过订阅者集合并退出 for 循环。

createInfoIndexFile

收集和过滤完订阅者后,接下来就可以生成 Index 类了。

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());
        // 包名不为空,写入包名
        if (myPackage != null) {
            writer.write("package " + myPackage + ";\n\n");
        }
        
        // 导包
        writer.write("import com.yarward.org.greenrobot.eventbus.meta.SimpleSubscriberInfo;\n");
        writer.write("import com.yarward.org.greenrobot.eventbus.meta.SubscriberMethodInfo;\n");
        writer.write("import com.yarward.org.greenrobot.eventbus.meta.SubscriberInfo;\n");
        writer.write("import com.yarward.org.greenrobot.eventbus.meta.SubscriberInfoIndex;\n\n");
        writer.write("import com.yarward.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");
        
        // 类声明,实现 `SubscriberInfoIndex` 接口
        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");
        
        // 写入 Index
        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");
        
        // 实现 `SubscriberInfoIndex` 接口方法
        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 {
        if (writer != null) {
            try {
                writer.close();
            } catch (IOException e) {
                //Silent
            }
        }
    }
}
复制代码

createInfoIndexFile 方法比较简单,主要是模板代码,生成 Index 的逻辑主要在 writeIndexLines 中:

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");
        }
    }
}
复制代码

writeIndexLines 方法也比较简单,遍历之前收集的订阅者集合,首先判断是否忽略当前订阅者,随后获取订阅者类名,如果是内部类则包含外部类的类名,然后再次判断当前订阅者是否可见,如果可见则生成 SimpleSubscriberInfo 对象添加进 Index。

SimpleSubscriberInfo 是 Index 元素,其构造方法有三个入参,第一个入参是:订阅者 Class 对象,第二个入参是:是否检查父类,默认为 true,第三个参数是:订阅方法信息数组。

接下来调用 writeCreateSubscriberMethods 方法生成订阅方法信息 SubscriberMethodInfo

private void writeCreateSubscriberMethods(BufferedWriter writer, List<ExecutableElement> methods,
                                          String callPrefix, String myPackage) throws IOException {
    // 遍历订阅方法集合
    for (ExecutableElement method : methods) {
        List<? extends VariableElement> parameters = method.getParameters();
        TypeMirror paramType = getParamTypeMirror(parameters.get(0), null);
        TypeElement paramElement = (TypeElement) processingEnv.getTypeUtils().asElement(paramType);
        // 获取订阅方法名称
        String methodName = method.getSimpleName().toString();
        // 获取 Event 事件 Class 对象
        String eventClass = getClassString(paramElement, myPackage) + ".class";

        // 获取 `Subscribe` 注解
        Subscribe subscribe = method.getAnnotation(Subscribe.class);
        List<String> parts = new ArrayList<>();
        parts.add(callPrefix + "(\"" + methodName + "\",");
        String lineEnd = "),";
        // 获取 `Subscribe` 注解的参数,并根据参数生成 `SubscriberMethodInfo` 的入参
        if (subscribe.priority() == 0 && !subscribe.sticky()) {
            if (subscribe.threadMode() == ThreadMode.POSTING) {
                parts.add(eventClass + lineEnd);
            } else {
                parts.add(eventClass + ",");
                parts.add("ThreadMode." + subscribe.threadMode().name() + lineEnd);
            }
        } else {
            parts.add(eventClass + ",");
            parts.add("ThreadMode." + subscribe.threadMode().name() + ",");
            parts.add(subscribe.priority() + ",");
            // 此处是笔者对必达事件的处理
            // begin - 添加必达事件
            parts.add(subscribe.sticky() + ",");
            parts.add(subscribe.rendezvous() + lineEnd);
            // end - 添加必达事件
        }
        writeLine(writer, 3, parts.toArray(new String[parts.size()]));

        if (verbose) {
            processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Indexed @Subscribe at " +
                                                     method.getEnclosingElement().getSimpleName() + "." + methodName +
                                                     "(" + paramElement.getSimpleName() + ")");
        }
    }
}
复制代码

遍历当前订阅者中所有的订阅方法,首先订阅方法的名称,入参 Event 事件的 Class 对象,然后获取 @Subscribe 注解的参数,并根据注解的参数生成 SubscriberMethodInfo 的入参,这里有笔者对必达事件的处理(注意:这里处理有Bug,不知读者有没有看出来呢?),最后调用 writeLine 完整的生成 SubscriberMethodInfo 方法。

至此,EventBus 注解处理器的流程分析完毕,下面是生成的 Index 类示例:

public class EventBusTestsIndex implements SubscriberInfoIndex {
    private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;

    static {
        SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>();

        putIndex(new SimpleSubscriberInfo(EventBusMainThreadTest.class, true, new SubscriberMethodInfo[] {
            new SubscriberMethodInfo("onEventMainThread", String.class, ThreadMode.MAIN),
        }));
    }

    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;
        }
    }
}
复制代码

总结

  • 在第一篇 Android-EventBus修改纪实 文章中笔者记录了为 EventBus 增加 必达事件 的过程,
  • 在第二篇 Android-EventBus修改纪实(二) 文章中笔者对第一篇文章中未实现的部分进行了补充,并简单介绍了 EventBus 提供的线程模型及使用注意事项,
  • 在第三篇 Android-EventBus修改纪实(三) 文章中笔者详细分析了 EventBus 的线程调度过程及使用注意事项,
  • 在第四篇文章中笔者详细分析了 EventBus 注解处理器生成 Index 类的流程,第一篇文章中添加 必达事件 时没有修改注解处理器,在本文中也进行了简单的描述。

本文分析订阅者是否符合规范时,有一处根据订阅方法检查过滤订阅者,如果订阅者中有一个方法不符合规范,整个订阅者都要跳过,这里你认为是否合理呢?

大家求同存异,欢迎交流。

happy~,希望可以帮你更好的使用 EventBus

猜你喜欢

转载自juejin.im/post/7105260722283610119