EventBus索引文件的生成

网上关于EventBus的分析已经很多,尤其是EventBus的订阅以及事件发送、接收相关的内容。这里不分析该部分的内容,仅分析一下索引文件是如何生成的。

ps:本文源码分析基于EventBus 3.3.1

涉及到的类

EventBus索引文件生成涉及的类

使用方法

// build.gradle
android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                // 配置索引文件名
                arguments = [eventBusIndex: 'com.xxx.yyy.HelloEventBusIndex']
            }
        }
    }
}

// xxx.java
EventBus.builder().addIndex(new com.xxx.yyy.HelloEventBusIndex()).build();
或
EventBus.builder().addIndex(new com.xxx.yyy.HelloEventBusIndex()).installDefaultEventBus();

索引文件的生成

// 此注解处理器支持的注解类型是"org.greenrobot.eventbus.Subscribe"
@SupportedAnnotationTypes("org.greenrobot.eventbus.Subscribe”)

// 此注解处理器支持的参数
@SupportedOptions(value = {"eventBusIndex", "verbose”})

// 用于构建增量注解处理器的注解,帮助我们生成了 /META-INF/gradle/注解 目录和文件的功能,相关可参考https://github.com/tbroyer/gradle-incap-helper
@IncrementalAnnotationProcessor(AGGREGATING)
public class EventBusAnnotationProcessor extends AbstractProcessor {
    
    // build.gradle中eventBus配置的arguments的key
    // eventBus索引文件的key,通知该key可获取对应的索引文件名(全限定名,即包含包路径),该索引文件是SubscriberInfoIndex的子类
    public static final String OPTION_EVENT_BUS_INDEX = "eventBusIndex”;

    // 是否通过Messager打印相关日志,后面日志相关的代码省略
    public static final String OPTION_VERBOSE = "verbose”;

    // 获取订阅类的所有订阅方法(不包含其父类),key是订阅类对应的TypeElement,value是订阅类的所有订阅方法对应的ExecutableElement的集合
    private final ListMap<TypeElement, ExecutableElement> methodsByClass = new ListMap<>();

    // 无效订阅类对应的TypeElement集合
    private final Set<TypeElement> classesToSkip = new HashSet<>();

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
        Messager messager = processingEnv.getMessager();
        try {
            String index = processingEnv.getOptions().get(OPTION_EVENT_BUS_INDEX);
            if (index == null) {
                // build.gradle中没有设置eventBusIndex
                return false;
            }
            verbose = Boolean.parseBoolean(processingEnv.getOptions().get(OPTION_VERBOSE));

            int lastPeriod = index.lastIndexOf('.’);
            
            // 索引文件的包名
            String indexPackage = lastPeriod != -1 ? index.substring(0, lastPeriod) : null;


            round++;

            // 省略一些异常情况…

            // 收集订阅类和方法
            collectSubscribers(annotations, env, messager);

            // 检查无效的订阅类和方法
            checkForSubscribersToSkip(messager, indexPackage);


            if (!methodsByClass.isEmpty()) {
                // 创建索引文件并输出内容
                createInfoIndexFile(index);
            } else {
                messager.printMessage(Diagnostic.Kind.WARNING, "No @Subscribe annotations found");
            }
            writerRoundDone = true;
        } catch (RuntimeException e) {
            // ....
        }
        return true;
    }

    // 收集订阅类和方法
    private void collectSubscribers(Set<? extends TypeElement> annotations, RoundEnvironment env, Messager messager) {
        for (TypeElement annotation : annotations) {
            // 1.获取被@Subscribe修饰的元素
            Set<? extends Element> elements = env.getElementsAnnotatedWith(annotation);
            for (Element element : elements) {
                // 2.如果该元素是方法(ExecutableElement表示类或者接口中的方法,构造函数或者初始化器)
                if (element instanceof ExecutableElement) {
                    ExecutableElement method = (ExecutableElement) element;
                    // 3.如果方法是public 且 不是static 且 方法参数只有一个,则是有效方法
                    if (checkHasNoErrors(method, messager)) {
                        // Element.getEnclosingElement()获取Element的父元素
                        // 4.获取method元素的父元素,即类(TypeElement)
                        TypeElement classElement = (TypeElement) method.getEnclosingElement();

                        // 5. 将订阅方法对应的元素放入对应的集合中
                        methodsByClass.putElement(classElement, method);
                    }
                } 
            }
        }
    }

    // 检测方法是否合法
    private boolean checkHasNoErrors(ExecutableElement element, Messager messager) {
        // 1.订阅方法是否static
        if (element.getModifiers().contains(Modifier.STATIC)) {
            return false;
        }

        // 2.订阅方法是否public
        if (!element.getModifiers().contains(Modifier.PUBLIC)) {
            return false;
        }

        
        // 3.订阅方法参数是否只有一个
        List<? extends VariableElement> parameters = ((ExecutableElement) element).getParameters();
        if (parameters.size() != 1) {
            return false;
        }
        return true;
    }

    // 检查无效的订阅类和方法
    private void checkForSubscribersToSkip(Messager messager, String myPackage) {
        // 1.遍历订阅类
        for (TypeElement skipCandidate : methodsByClass.keySet()) {
            TypeElement subscriberClass = skipCandidate;
            while (subscriberClass != null) {
                // 2.如果订阅类对于索引类不可见,则添加到classesToSkip集合中
                // 无效的条件:如果订阅类及其父类不是puhlic的 或 订阅类及其父类的包名和eventBusIndex的包名不一样,则该订阅类无效,需要忽略
                if (!isVisible(myPackage, subscriberClass)) {
                    boolean added = classesToSkip.add(skipCandidate);
                    // ...
                    break;
                }
                List<ExecutableElement> methods = methodsByClass.get(subscriberClass);
                if (methods != null) {
                    // 3.遍历订阅方法,如果方法参数不合法则添加到classesToSkip集合中
                    for (ExecutableElement method : methods) {
                        String skipReason = null;
                        VariableElement param = method.getParameters().get(0);
                        // 获取参数类型
                        TypeMirror typeMirror = getParamTypeMirror(param, messager);
                        // 3.1 如果方法参数类型不是一个类或接口类型,则无法解析处理,则该订阅类无效,需要忽略
                        if (!(typeMirror instanceof DeclaredType) ||
                            !(((DeclaredType) typeMirror).asElement() instanceof TypeElement)) {
                            skipReason = "event type cannot be processed";
                        }
                        if (skipReason == null) {
                            TypeElement eventTypeElement = (TypeElement) ((DeclaredType) typeMirror).asElement();
                            // 3.2 如果方法参数对应的类及其父类不是public的 或 其包名和eventBusIndex的包名不一样,则该订阅类无效,需要忽略
                            if (!isVisible(myPackage, eventTypeElement)) {
                                skipReason = "event type is not public";
                            }
                        }
                        if (skipReason != null) {
                            boolean added = classesToSkip.add(skipCandidate);
                            // ...
                            break;
                        }
                    }
                }
                // 4.获取父类,接着遍历父类的订阅方法
                subscriberClass = getSuperclass(subscriberClass);
            }
        }
    }

    // 判断typeElement对应的类对于索引类是否可见
    private boolean isVisible(String myPackage, TypeElement typeElement) {
        // 获取类的修饰符
        Set<Modifier> modifiers = typeElement.getModifiers();
        boolean visible;
        if (modifiers.contains(Modifier.PUBLIC)) {
            // 如果类是public的,则对于索引类是可见的
            visible = true;
        } else if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.PROTECTED)) {
            // 如果类是private或protected的,则对于索引类是不可见的
            visible = false;
        } else {
            // 如果类的包名和索引类的包名一样,即在同一个包目录下,则对于索引类是可见的
            String subscriberPackage = getPackageElement(typeElement).getQualifiedName().toString();
            if (myPackage == null) {
                visible = subscriberPackage.length() == 0;
            } else {
                visible = myPackage.equals(subscriberPackage);
            }
        }
        return visible;
    }

    // 获取父类元素
    private TypeElement getSuperclass(TypeElement type) {
        // TypeElement.getSuperclass() 返回此类型元素的直接超类。如果此类型元素表示一个接口或者类java.lang.Object,则返回一个种类为 NONE 的 NoType
        // TypeKind.DECLARED表示一个类或接口类型

        // 如果type的直接超类是一个类或接口,且不是java.或javax.或android.包下的系统类,则返回它的直接超类
        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.")) {
                // Skip system classes, this just degrades performance
                return null;
            } else {
                return superclass;
            }
        } else {
            return null;
        }
    }

    // 创建索引文件并输出内容
    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 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 {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    //Silent
                }
            }
        }
    }

    // 将订阅方法输出到索引文件
    private void writeIndexLines(BufferedWriter writer, String myPackage) throws IOException {
        // 1.遍历订阅类
        for (TypeElement subscriberTypeElement : methodsByClass.keySet()) {
            // 2.如果是无效订阅类(例如订阅类或订阅方法参数对应的类对于索引文件不可见),则跳过该类的订阅方法
            if (classesToSkip.contains(subscriberTypeElement)) {
                continue;
            }

            // 2.获取订阅类的全限定名(即 包名.类名 )
            String subscriberClass = getClassString(subscriberTypeElement, myPackage);

            // 3.如果订阅类对应索引类可见,则写入内容到索引文件
            if (isVisible(myPackage, subscriberTypeElement)) {
                writeLine(writer, 2,
                        "putIndex(new SimpleSubscriberInfo(" + subscriberClass + ".class,",
                        "true,", "new SubscriberMethodInfo[] {");
                List<ExecutableElement> methods = methodsByClass.get(subscriberTypeElement);

                // 4.写入SubscriberMethodInfo到索引类中,省略该部分内容,主要就是写入订阅方法名,参数类型,ThreadMode,priority、sticky。
                writeCreateSubscriberMethods(writer, methods, "new SubscriberMethodInfo", myPackage);
                writer.write("        }));\n\n");
            } else {
                writer.write("        // Subscriber not visible to index: " + subscriberClass + "\n");
            }
        }
    }
}

总结:

  1. eventBus注解处理器只收集被@Subscribe修饰的、public的、非static的、只有一个参数的方法
  2. 如果订阅类及其父类对于索引类不可见,则忽略该订阅类
  3. 如果订阅方法的参数类型不是一个类或接口类型,则无法解析处理,忽略该订阅类
  4. 如果订阅方法的参数对应的类及其父类对于索引类不可见,则忽略该订阅类

ps:
关于编译时注解的一些参考资料:

  1. Element元素及其子类和方法
  2. Element与TypeMirror的关系
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335

推荐阅读更多精彩内容