EventBus全解析系列(四)

Subscriber Index

背景

  • Subscriber Index是EventBus 3.0新引入的功能猜极,它能大幅提升初始注冊過程的效率。其原理其實很簡單素跺,就是把運行時期做的事情放到了編譯時期去做,從而提升了運行時期的效率。大家可以從EventBus作者的博客中的對比圖得到感性的認(rèn)識疲牵。 可以看到,開啟了索引的3.0榆鼠,其注冊速度比2.4快了3到6倍纲爸。
    the resulting speed in registrations per second (higher is better)
  • 然而,該新功能是一個可選項妆够,使用者必須通過一定的配置才能使得該新功能得以生效识啦。

前提

  • 訂閱者和訂閱事件類必須是Public的,訂閱者方法才能被索引到神妹;另外颓哮,由于Java注解機制自身的限制,在匿名類中的@Subscribe注解是不能被識別的鸵荠,從而不能生成索引冕茅。
  • 但這不會造成致命問題,因為在注冊過程中存在這樣的邏輯:如果使用不了索引蛹找,那么程序?qū)⒆詣邮褂梅瓷錂C制姨伤。雖然這又回到了運行時期做事情的老路上,使得性能下降庸疾,但EventBus依然可以正常工作乍楚。

配置

Android Gradle 插件2.2.0之前,我們使用android-apt的方式彼硫,在編譯期生成Subscriber Index類炊豪。所謂APT,即Annotation Processing Tool拧篮,其結(jié)合EventBus提供的EventBusAnnotationProcessor词渤,就可以就在編譯期,對源代碼文件進(jìn)行檢測串绩,找出其中的Annotation缺虐,從而生成目標(biāo)文件,即Subscriber Index類礁凡。
然而高氮,畢竟android-apt是第三方插件慧妄,自Android Gradle 插件2.2.0 之后,其官方提供了相同的能力剪芍,即annotationProcessor 塞淹,來完全代替 android-apt,配置起來更簡單罪裹,推薦大家使用饱普。

1.使用android-apt
buildscript {
    dependencies {
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}
 
apply plugin: 'com.neenbedankt.android-apt'
 
dependencies {
    compile 'org.greenrobot:eventbus:3.0.0'
    apt 'org.greenrobot:eventbus-annotation-processor:3.0.1'
}
 
apt {
    arguments {
        eventBusIndex "com.monster.android.wild.MyEventBusIndex"
    }
}

com.monster.android.wild.MyEventBusIndex就是我們想要生成的Subscriber Index類

2.使用annotationProcessor
android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [eventBusIndex:'com.monster.android.wild.MyEventBusIndex']
            }
        }
    }
}

dependencies {
    compile 'org.greenrobot:eventbus:3.0.0'
    annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.0.1'
}   

同上,com.monster.android.wild.MyEventBusIndex就是我們想要生成的Subscriber Index類状共√赘可以看到,這樣配置起來更簡單峡继。

3.代碼配置

在build.gradle中配置完畢之后冯袍,再進(jìn)行編譯,Subscriber Index類MyEventBusIndex就會自動為我們生成了碾牌。
注意康愤,我們的工程中需要有使用EventBus相關(guān)的代碼,才能生成哦舶吗。

MyEventBusIndex_Path.JPG

接下來翘瓮,在代碼中,如果我們想使用Subscriber Index裤翩,就可以在構(gòu)造EventBus時资盅,如下這樣

EventBus eventBus = EventBus.builder().addIndex(new MyEventBusIndex()).build();

但如果每次都這樣寫一大串,未免有些繁瑣。既然我們引入了Subscriber Index,我們當(dāng)然希望我們的工程中全部使用這樣方式莽鸭,這時瞬哼,就可以這樣

// 只設(shè)置一次,并且要在我們第一次使用EventBus之前進(jìn)行設(shè)置
EventBus.builder().addIndex(new MyEventBusIndex()).installDefaultEventBus();
// 這樣每次獲取到默認(rèn)實例,都是使用Subscriber Index的了,代碼得到了精簡。
EventBus eventBus = EventBus.getDefault();

源代碼

1.Subscriber Index類MyEventBusIndex是如何生成的

首先蓝晒,我們需要明確,MyEventBusIndex是在編譯時期帖鸦,通過第三方提供的android-apt或者Gradle本身提供的annotationProcessor芝薇,結(jié)合EventBus提供的EventBusAnnotationProcessor,共同協(xié)作而生成的作儿。而EventBusAnnotationProcessor由EventBus提供洛二,也就說明具體生成邏輯是由EventBus控制,這算是經(jīng)典的模板模式。
下面晾嘶,我們大致看一下EventBusAnnotationProcessor是如何定義生成邏輯并生成目標(biāo)Java類文件的妓雾。大家可以從EventBus 的Github上面看到完整源代碼,這里只是簡要的闡述:

@SupportedAnnotationTypes("org.greenrobot.eventbus.Subscribe")
@SupportedOptions(value = {"eventBusIndex", "verbose"})
public class EventBusAnnotationProcessor extends AbstractProcessor {

    // process()是該類的核心函數(shù)垒迂,就是在這里械姻,實現(xiàn)收集和評估注解的代碼,以及生成Java文件
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
    // 保存訂閱者的訂閱方法信息
    private final ListMap<TypeElement, ExecutableElement> methodsByClass = new ListMap<>();
    // 保存需要跳過的訂閱者
    private final Set<TypeElement> classesToSkip = new HashSet<>();
    
        try {
            // 從build.gradle中讀取到arguments机断,即com.monster.android.wild.MyEventBusIndex
            String index = processingEnv.getOptions().get(OPTION_EVENT_BUS_INDEX);
            // 收集策添,填充methodsByClass 
            collectSubscribers(annotations, env, messager);
            // 評估,填充classesToSkip 
            checkForSubscribersToSkip(messager, indexPackage);

            if (!methodsByClass.isEmpty()) {
                // 生成java文件(根據(jù)methodsByClass和classesToSkip)
                createInfoIndexFile(index);
            } else {
            }
        } catch (RuntimeException e) {
        }
        return true;
    }
}

    // 生成java文件毫缆,其中有一部分是hardcode
    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) {
        } finally {
        }
    }

接下來我們看看生成的Subscriber Index類MyEventBusIndex

/** This class is generated by EventBus, do not edit. */
public class MyEventBusIndex implements SubscriberInfoIndex {
    private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;

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

        // 從以上createInfoIndexFile()的實現(xiàn)來看,除了類名MyEventBusIndex乐导,只有這一段代碼是非hardcode苦丁。
        // 以訂閱者類為單位,將該訂閱者類中的所有訂閱函數(shù)以及相關(guān)參數(shù)物臂,封裝到SimpleSubscriberInfo類對象中旺拉,
        // 以供EventBus在注冊過程中使用。注意SimpleSubscriberInfo類對象是在編譯時期就生成的棵磷,
        // 因而在運行時期可以直接使用蛾狗,省去了運行時期通過反射做相似操作的時間和資源消耗,從而提高效率仪媒,這里就是全文的精髓所在沉桌。
        putIndex(new SimpleSubscriberInfo(com.monster.android.wild.myeventbusdemo.MainActivity.class, true,
                new SubscriberMethodInfo[] {
            new SubscriberMethodInfo("onEvent",
                    com.monster.android.wild.myeventbusdemo.MainActivity.EventBusEvent.class, ThreadMode.MAIN),
        }));

    }

    private static void putIndex(SubscriberInfo info) {
        SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info);
    }

    // 將會在EventBus在注冊過程中使用,等會大家會看到
    @Override
    public SubscriberInfo getSubscriberInfo(Class<?> subscriberClass) {
        SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);
        if (info != null) {
            return info;
        } else {
            return null;
        }
    }
}
2.EventBus是如何使用Subscriber Index類MyEventBusIndex的

我們知道算吩,在EventBus注冊的時候留凭,需要調(diào)用SubscriberMethodFinder的findSubscriberMethods()方法。忽略無關(guān)代碼如下:

    List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
        if (ignoreGeneratedIndex) {
            subscriberMethods = findUsingReflection(subscriberClass);
        } else {
            subscriberMethods = findUsingInfo(subscriberClass);
        }
    }

默認(rèn)情況下偎巢,ignoreGeneratedIndex為false蔼夜,所以會調(diào)用findUsingInfo()

    private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
        FindState findState = prepareFindState();
        findState.initForSubscriber(subscriberClass);
        while (findState.clazz != null) {
            // 焦點調(diào)用在這里,如何通過索引獲取訂閱者信息
            findState.subscriberInfo = getSubscriberInfo(findState);
            if (findState.subscriberInfo != null) {
                SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();
                for (SubscriberMethod subscriberMethod : array) {
                    if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {
                        findState.subscriberMethods.add(subscriberMethod);
                    }
                }
            } else {
                // 這一點與上文提到的相呼應(yīng):索引找不到压昼,還可以回到老路上求冷,即,使用反射來找窍霞,
                // 雖然性能會降低匠题,但保證EventBus仍然可以正常工作
                findUsingReflectionInSingleClass(findState);
            }
            findState.moveToSuperclass();
        }
        return getMethodsAndRelease(findState);
    }

順著焦點調(diào)用,我們繼續(xù)往下看

    private SubscriberInfo getSubscriberInfo(FindState findState) {
        if (findState.subscriberInfo != null && findState.subscriberInfo.getSuperSubscriberInfo() != null) {
            SubscriberInfo superclassInfo = findState.subscriberInfo.getSuperSubscriberInfo();
            if (findState.clazz == superclassInfo.getSubscriberClass()) {
                return superclassInfo;
            }
        }
        // 焦點在這里但金,輪詢subscriberInfoIndexes得到SubscriberInfoIndex對象梧躺,從而得到SubscriberInfo對象
        if (subscriberInfoIndexes != null) {
            for (SubscriberInfoIndex index : subscriberInfoIndexes) {
                SubscriberInfo info = index.getSubscriberInfo(findState.clazz);
                if (info != null) {
                    return info;
                }
            }
        }
        return null;
    }

插一句,那subscriberInfoIndexes如何得來的呢?還記得掠哥,我們在代碼配置的時候調(diào)用過

EventBus eventBus = EventBus.builder().addIndex(new MyEventBusIndex()).build(); 

索引就是這個時候傳給EventBus的巩踏,我們看一下源碼:

    /** Adds an index generated by EventBus' annotation preprocessor. */
    public EventBusBuilder addIndex(SubscriberInfoIndex index) {
        if(subscriberInfoIndexes == null) {
            subscriberInfoIndexes = new ArrayList<>();
        }
        subscriberInfoIndexes.add(index);
        return this;
    }

再回來,所以得到的SubscriberInfoIndex對象就是我們的MyEventBusIndex類對象续搀。接著調(diào)用MyEventBusIndex類對象的getSubscriberInfo()方法塞琼,從而得到SubscriberInfo對象,而這正與MyEventBusIndex源代碼中“等會大家會看到”的注解相呼應(yīng)禁舷!
至此彪杉,代碼邏輯就捋順了~~~

3.總結(jié)

到這里,如何生成Subscriber Index類以及如何使用Subscriber Index類就分析完畢了牵咙。分析過程中派近,大家應(yīng)該就可以看到了,核心思想就是把運行時所需要做的耗時操作轉(zhuǎn)移到了編譯時期去做洁桌,從而提高了整體性能渴丸。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市另凌,隨后出現(xiàn)的幾起案子谱轨,更是在濱河造成了極大的恐慌,老刑警劉巖吠谢,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件土童,死亡現(xiàn)場離奇詭異,居然都是意外死亡工坊,警方通過查閱死者的電腦和手機献汗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來王污,“玉大人雀瓢,你說我怎么就攤上這事∮竦В” “怎么了刃麸?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長司浪。 經(jīng)常有香客問我泊业,道長,這世上最難降的妖魔是什么啊易? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任吁伺,我火速辦了婚禮,結(jié)果婚禮上租谈,老公的妹妹穿的比我還像新娘篮奄。我一直安慰自己捆愁,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布窟却。 她就那樣靜靜地躺著昼丑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪夸赫。 梳的紋絲不亂的頭發(fā)上菩帝,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天,我揣著相機與錄音茬腿,去河邊找鬼呼奢。 笑死,一個胖子當(dāng)著我的面吹牛切平,可吹牛的內(nèi)容都是我干的握础。 我是一名探鬼主播,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼悴品,長吁一口氣:“原來是場噩夢啊……” “哼禀综!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起他匪,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎夸研,沒想到半個月后邦蜜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡亥至,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年悼沈,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片姐扮。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡絮供,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出茶敏,到底是詐尸還是另有隱情壤靶,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布惊搏,位于F島的核電站贮乳,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏恬惯。R本人自食惡果不足惜向拆,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望酪耳。 院中可真熱鬧浓恳,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至吆鹤,卻和暖如春厨疙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背疑务。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工沾凄, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人知允。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓撒蟀,卻偏偏與公主長得像,于是被迫代替她去往敵國和親温鸽。 傳聞我的和親對象是個殘疾皇子保屯,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,789評論 25 707
  • 博文出處:EventBus源碼解析,歡迎大家關(guān)注我的博客涤垫,謝謝姑尺! 0001B 時近年末,但是也沒閑著蝠猬。最近正好在看...
    俞其榮閱讀 1,298評論 1 16
  • 對于Android開發(fā)老司機來說肯定不會陌生切蟋,它是一個基于觀察者模式的事件發(fā)布/訂閱框架,開發(fā)者可以通過極少的代碼...
    飛揚小米閱讀 1,473評論 0 50
  • EventBus 簡介 EventBus 直譯過來就是事件總線榆芦,熟悉計算機原理的人一定很熟悉總線的概念柄粹,所有設(shè)備都...
    DanieX閱讀 1,042評論 0 1
  • 照片選擇
    coderJerry01閱讀 206評論 0 0