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閱讀 230,622評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,677評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,643評論 2 380

推薦閱讀更多精彩內容