Bugly 技術乾貨系列內容主要涉及移動開發方向,是由 Bugly 邀請騰訊內部各位技術大咖,經過平常工做經驗的總結以及感悟撰寫而成,內容均屬原創,轉載請標明出處。vue
EventBus對於Android開發老司機來講確定不會陌生,它是一個基於觀察者模式的事件發佈/訂閱框架,開發者能夠經過極少的代碼去實現多個模塊之間的通訊,而不須要以層層傳遞接口的形式去單獨構建通訊橋樑。從而下降因多重回調致使的模塊間強耦合,同時避免產生大量內部類。它擁有使用方便,性能高,接入成本低和支持多線程的優勢,實乃模塊解耦、代碼重構必備良藥。java
做爲Markus Junginger大神耗時4年打磨、超過1億接入量、Github 9000+ star的明星級組件,分析EventBus的文章早已經是數不勝數。本文的題目是「教你飆巴士」,而這輛Bus之因此能夠飆起來,是由於做者在EventBus 3中引入了EventBusAnnotationProcessor(註解分析生成索引)技術,大大提升了EventBus的運行效率。而分析這個加速器的資料在網上不多,所以本文會把重點放在分析這個EventBus 3的新特性上,同時分享一些踩坑經驗,並結合源碼分析及UML圖,以直觀的形式和你們一塊兒學習EventBus 3的用法及運行原理。android
打開App的build.gradle,在dependencies中添加最新的EventBus依賴:c++
compile 'org.greenrobot:eventbus:3.0.0'
若是不須要索引加速的話,就能夠直接跳到第二步了。而要應用最新的EventBusAnnotationProcessor則比較麻煩,由於註解解析依賴於android-apt-plugin。咱們一步一步來,首先在項目gradle的dependencies中引入apt編譯插件:git
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
而後在App的build.gradle中應用apt插件,並設置apt生成的索引的包名和類名:github
apply plugin: 'com.neenbedankt.android-apt' apt { arguments { eventBusIndex "com.study.sangerzhong.studyapp.MyEventBusIndex" } }
接着在App的dependencies中引入EventBusAnnotationProcessor:編程
apt 'org.greenrobot:eventbus-annotation-processor:3.0.1'
這裏須要注意,若是應用了EventBusAnnotationProcessor卻沒有設置arguments的話,編譯時就會報錯:No option eventBusIndex passed to annotation processor
數組
此時須要咱們先編譯一次,生成索引類。編譯成功以後,就會發如今\ProjectName\app\build\generated\source\apt\PakageName\
下看到經過註解分析生成的索引類,這樣咱們即可以在初始化EventBus時應用咱們生成的索引了。緩存
EventBus默認有一個單例,能夠經過getDefault()
獲取,也能夠經過EventBus.builder()
構造自定義的EventBus,好比要應用咱們生成好的索引時:微信
EventBus mEventBus = EventBus.builder().addIndex(new MyEventBusIndex()).build();
若是想把自定義的設置應用到EventBus默認的單例中,則能夠用installDefaultEventBus()
方法:
EventBus.builder().addIndex(new MyEventBusIndex()).installDefaultEventBus();
全部能被實例化爲Object的實例均可以做爲事件:
public class DriverEvent { public String info; }
在最新版的eventbus 3中若是用到了索引加速,事件類的修飾符必須爲public,否則編譯時會報錯:Subscriber method must be public
首先把做爲訂閱事件的模塊經過EventBus註冊監聽:
mEventBus.register(this);
在3.0以前,註冊監聽須要區分是否監聽黏性(sticky)事件,監聽EventBus事件的模塊須要實現以onEvent開頭的方法。現在改成在方法上添加註解的形式:
@Subscribe(threadMode = ThreadMode.POSTING, priority = 0, sticky = true) public void handleEvent(DriverEvent event) { Log.d(TAG, event.info); }
註解有三個參數,threadMode爲回調所在的線程,priority爲優先級,sticky爲是否接收黏性事件。調度單位從類細化到了方法,對方法的命名也沒有了要求,方便混淆代碼。但註冊了監聽的模塊必須有一個標註了Subscribe註解方法,否則在register時會拋出異常:Subscriber class XXX and its super classes have no public methods with the @Subscribe annotation
調用post或者postSticky便可:
mEventBus.post(new DriverEvent("magnet:?xt=urn:btih……"));
到此咱們就完成了使用EventBus的學習,能夠在代碼中盡情地飈車了。項目接入了EventBus以後會有什麼好處呢?舉一個常見的用例:ViewPager中Fragment的相互通訊,就不須要在容器中定義各類接口,能夠直接經過EventBus來實現相互回調,這樣就把邏輯從ViewPager這個容器中剝離出來,使代碼閱讀起來更加直觀。
在實際項目的使用中,register和unregister一般與Activity和Fragment的生命週期相關,ThreadMode.MainThread能夠很好地解決Android的界面刷新必須在UI線程的問題,不須要再回調後用Handler中轉(EventBus中已經自動用Handler作了處理),黏性事件能夠很好地解決post與register同時執行時的異步問題(這個在原理中會說到),事件的傳遞也沒有序列化與反序列化的性能消耗,足以知足咱們大部分狀況下的模塊間通訊需求。
在平時使用中咱們不須要關心EventBus中對事件的分發機制,但要成爲可以快速排查問題的老司機,咱們仍是得熟悉它的工做原理,下面咱們就透過UML圖來學習一下。
EventBus的核心工做機制透過做者Blog中的這張圖就能很好地理解:
訂閱者模塊須要經過EventBus訂閱相關的事件,並準備好處理事件的回調方法,而事件發佈者則在適當的時機把事件post出去,EventBus就能幫咱們搞定一切。在架構方面,EventBus 3與以前稍老版本有不一樣,咱們直接看架構圖:
先看核心類EventBus,其中subscriptionByEventType是以事件的類爲key,訂閱者的回調方法爲value的映射關係表。也就是說EventBus在收到一個事件時,就能夠根據這個事件的類型,在subscriptionByEventType中找到全部監聽了該事件的訂閱者及處理事件的回調方法。而typesBySubscriber則是每一個訂閱者所監聽的事件類型表,在取消註冊時能夠經過該表中保存的信息,快速刪除subscriptionByEventType中訂閱者的註冊信息,避免遍歷查找。註冊事件、發送事件和註銷都是圍繞着這兩個核心數據結構來展開。上面的Subscription能夠理解爲每一個訂閱者與回調方法的關係,在其餘模塊發送事件時,就會經過這個關係,讓訂閱者執行回調方法。
回調方法在這裏被封裝成了SubscriptionMethod,裏面保存了在須要反射invoke方法時的各類參數,包括優先級,是否接收黏性事件和所在線程等信息。而要生成這些封裝好的方法,則須要SubscriberMethodFinder,它能夠在regster時獲得訂閱者的全部回調方法,並封裝返回給EventBus。而右邊的加速器模塊,就是爲了提升SubscriberMethodFinder的效率,會在第三章詳細介紹,這裏就再也不囉嗦。
而下面的三個Poster,則是EventBus能在不一樣的線程執行回調方法的核心,咱們根據不一樣的回調方式來看:
能夠看到,不一樣的Poster會在post事件時,調度相應的事件隊列PendingPostQueue,讓每一個訂閱者的回調方法收到相應的事件,並在其註冊的Thread中運行。而這個事件隊列是一個鏈表,由一個個PendingPost組成,其中包含了事件,事件訂閱者,回調方法這三個核心參數,以及須要執行的下一個PendingPost。
至此EventBus 3的架構就分析完了,與以前EventBus老版本最明顯的區別在於:分發事件的調度單位從訂閱者,細化成了訂閱者的回調方法。也就是說每一個回調方法都有本身的優先級,執行線程和是否接收黏性事件,提升了事件分發的靈活程度,接下來咱們在看核心功能的實現時更能體現這一點。
簡單來講就是:根據訂閱者的類來找回調方法,把訂閱者和回調方法封裝成關係,並保存到相應的數據結構中,爲隨後的事件分發作好準備,最後處理黏性事件:
值得注意的是,老版本的EventBus是容許事件訂閱者以不一樣的ThreadMode去監聽同一個事件的,即在一個訂閱者中有多個方法訂閱一個事件,此時是沒法保證這幾個回調的前後順序的,由於不一樣的線程回調是經過Handler調度的,有可能單個線程中的事件過多,事件受阻,回調則會比較慢。現在EventBus 3使用了註解來表示回調後,還能夠出現相同的ThreadMode的回調方法監聽相同的事件,此時會根據註冊的前後順序,先註冊先分發事件,注意不是先收到事件,收到事件的順序仍是得看poster中Handler的調度。
總的來講就是分析事件,獲得全部監聽該事件的訂閱者的回調方法,並利用反射來invoke方法,實現回調:
這裏就能看到poster的調度事件功能,同時能夠看到調度的單位細化成了Subscription,即每個方法都有本身的優先級和是否接收黏性事件。在源代碼中爲了保證post執行不會出現死鎖,等待和對同一訂閱者發送相同的事件,增長了不少線程保護鎖和標誌位,值得咱們每一個開發者學習。
註銷就比較簡單了,把在註冊時往兩個數據結構中添加的訂閱者信息刪除便可:
上面常常會提到黏性事件,爲何要有這個設計呢?這裏舉個例子:我想在登錄成功後自動播放歌曲,而登錄和監聽登錄監聽是同時進行的。在這個前提下,若是登錄流程走得特別快,在登錄成功後播放模塊才註冊了監聽。此時播放模塊便會錯過了【登錄成功】的事件,出現「雖然登錄成功了,回調卻沒執行」的狀況。而若是【登錄成功】這個事件是一個黏性事件的話,那麼即便我後來才註冊了監聽(而且回調方法設置爲監聽黏性事件),則回調就能在註冊的那一刻被執行,這個問題就能被優雅地解決,而不須要額外去定義其餘標誌位。
至此你們對EventBus的運行原理應該有了必定的瞭解,雖然看起來像是一個複雜耗時的自動機,但大部分時候事件都是一瞬間就能分發到位的,而你們關心的性能問題反而是發生在註冊EventBus的時候,由於須要遍歷監聽者的全部方法去找到回調的方法。做者也提到運行時註解的性能在Android上並不理想,爲了解決這個問題,做者纔會以索引的方式去生成回調方法表(下一章會詳細介紹)。而EventBus源碼分析的文章早已經是數不勝數,這裏就再也不大段大段地貼代碼了,主要以類圖和流程圖的形式讓你們直觀地瞭解EventBus3的總體架構及核心功能的實現原理,把源碼分析留到後面介紹EventBusAnnotationProcessor中再進行。你們若是想要深刻學習EventBus 3的話,在本文結尾的參考文章中有不少寫得很棒的源碼分析。
在EventBus 3的介紹中,做者提到之前的版本爲了保證性能,在遍歷尋找訂閱者的回調方法時使用反射而不是註解。但如今卻能在使用註解的前提下,大幅度提升性能,同時做者在博客中放出了這張對比圖:
能夠看到在性能方面,EventBus 3因爲使用了註解,比起使用反射來遍歷方法的2.4版本遜色很多。但開啓了索引後性能像打了雞血同樣,遠遠超出以前的版本。這裏咱們就來分析一下這個提升EventBus性能的「渦輪引擎」。(下面的源碼分析爲了方便閱讀,添加了部分註釋,並刪減了部分源碼,若是有疑問的話能夠到官方的github上查看原版源碼)
首先咱們知道,索引是在初始化EventBus時經過EventBusBuilder.addIndex(SubscriberInfoIndex index)
方法傳進來的,咱們就先看看這個方法:
public EventBusBuilder addIndex(SubscriberInfoIndex index) { if(subscriberInfoIndexes == null) { subscriberInfoIndexes = new ArrayList<>(); } subscriberInfoIndexes.add(index); return this; }
能夠看到,傳進來的索引信息會保存在subscriberInfoIndexes這個List中,後續會經過EventBusBuilder傳到相應EventBus的SubscriberMethodFinder實例中。咱們先來分析SubscriberInfoIndex這個參數:
public interface SubscriberInfoIndex { SubscriberInfo getSubscriberInfo(Class<?> subscriberClass); }
可見索引只須要作一件事情——就是能拿到訂閱者的信息。而實現這個接口的類若是咱們沒有編譯過,是找不到的。這裏就得看咱們在一開始在配置gradle時導入的EventBusAnnotationProcessor:
@SupportedAnnotationTypes("org.greenrobot.eventbus.Subscribe") @SupportedOptions(value = {"eventBusIndex", "verbose"}) public class EventBusAnnotationProcessor extends AbstractProcessor { /** Found subscriber methods for a class (without superclasses). 被註解表示的方法信息 */ private final ListMap<TypeElement, ExecutableElement> methodsByClass = new ListMap<>(); private final Set<TypeElement> classesToSkip = new HashSet<>(); // checkHasErrors檢查出來的異常方法 @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) { // 若是沒有在gradle中配置apt的argument,編譯就會在這裏報錯 messager.printMessage(Diagnostic.Kind.ERROR, "No option " + OPTION_EVENT_BUS_INDEX + " passed to annotation processor"); return false; } /** ... */ collectSubscribers(annotations, env, messager); // 根據註解拿到全部訂閱者的回調方法信息 checkForSubscribersToSkip(messager, indexPackage); // 篩掉不符合規則的訂閱者 if (!methodsByClass.isEmpty()) { createInfoIndexFile(index); // 生成索引類 } /** 打印錯誤 */ } /** 下面這些方法就再也不貼出具體實現了,咱們瞭解它們的功能就行 */ private void collectSubscribers // 遍歷annotations,找出全部被註解標識的方法,以初始化methodsByClass private boolean checkHasNoErrors // 過濾掉static,非public和參數大於1的方法 private void checkForSubscribersToSkip // 檢查methodsByClass中的各個類,是否存在非public的父類和方法參數 /** 下面這三個方法會把methodsByClass中的信息寫到相應的類中 */ private void writeCreateSubscriberMethods private void createInfoIndexFile private void writeIndexLines }
至此便揭開了索引生成的祕密,是在編譯時apt插件經過EventBusAnnotationProcessor分析註解,並利用註解標識的相關類的信息去生成相關的類。writeCreateSubscriberMethods中調用了不少IO函數,很容易理解,這裏就不貼了,咱們直接看生成出來的類:
/** 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>(); // 每有一個訂閱者類,就調用一次putIndex往索引中添加相關的信息 putIndex(new SimpleSubscriberInfo(com.study.sangerzhong.studyapp.ui.MainActivity.class, true, new SubscriberMethodInfo[] { new SubscriberMethodInfo("onEvent", com.study.sangerzhong.studyapp.ui.MainActivity.DriverEvent.class, ThreadMode.POSTING, 0, false), // 類中每個被Subscribe標識的方法都在這裏添加進來 })); } // 下面的代碼就是EventBusAnnotationProcessor中寫死的了 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; } } }
可見,子類中hardcode了全部註冊了EventBus的類中被Subscribe註解標識的方法信息,包括方法名、方法參數類型等信息。並把這些信息封裝到SimpleSubscriberInfo中,咱們拿到的索引其實就是以訂閱者的類爲Key、SimpleSubscriberInfo爲value的哈希表。而這些hardcode都是在編譯的時候生成的,避免了在在EventBus.register()時纔去遍歷查找生成,從而把在註冊時須要遍歷訂閱者全部方法的行爲,提早到在編譯時完成了。
索引的生成咱們已經明白了,那麼它是在哪裏被應用的呢?咱們記得在註冊時,EventBus會經過SubscriberMethodFinder來遍歷註冊對象的Class的全部方法,篩選出符合規則的方法,並做爲訂閱者在接收到事件時執行的回調,咱們直接來看看SubscriberMethodFinder.findSubscriberMethods()
這個核心方法:
List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) { List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass); if (subscriberMethods != null) { return subscriberMethods; // 先去方法緩存裏找,找到直接返回 } if (ignoreGeneratedIndex) { // 是否忽略設置的索引 subscriberMethods = findUsingReflection(subscriberClass); } else { subscriberMethods = findUsingInfo(subscriberClass); } /** 把找到的方法保存到METHOD_CACHE裏並返回,找不到直接拋出異常 */ }
能夠看到其中findUsingInfo()方法就是去索引中查找訂閱者的回調方法,咱們戳進去看看這個方法的實現:
private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) { // 最新版的EventBus3中,尋找方法時所需的臨時變量都被封裝到了FindState這個靜態內部類中 FindState findState = prepareFindState(); // 到對象池中取得上下文,避免頻繁創造對象,這個設計很贊 findState.initForSubscriber(subscriberClass); // 初始化尋找方法的上下文 while (findState.clazz != null) { // 子類找完了,會繼續去父類中找 findState.subscriberInfo = getSubscriberInfo(findState); // 得到訂閱者類的相關信息 if (findState.subscriberInfo != null) { // 上一步能拿到相關信息的話,就開始把方法數組封裝成List SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods(); for (SubscriberMethod subscriberMethod : array) { if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) { // checkAdd是爲了不在父類中找到的方法是被子類重寫的,此時應該保證回調時執行子類的方法 findState.subscriberMethods.add(subscriberMethod); } } } else { // 索引中找不到,降級成運行時經過註解和反射去找 findUsingReflectionInSingleClass(findState); } findState.moveToSuperclass(); // 上下文切換成父類 } return getMethodsAndRelease(findState); // 找完後,釋放FindState進對象池,並返回找到的回調方法 }
能夠看到EventBus中在查找訂閱者的回調方法時是能處理好繼承關係的,不只會去遍歷父類,並且還會避免由於重寫方法致使執行屢次回調。其中須要關心的是getSubscriberInfo()是如何返回索引數據的,咱們繼續深刻:
private SubscriberInfo getSubscriberInfo(FindState findState) { if (findState.subscriberInfo != null && findState.subscriberInfo.getSuperSubscriberInfo() != null) { // subscriberInfo已有實例,證實本次查找須要查找上次找過的類的父類 SubscriberInfo superclassInfo = findState.subscriberInfo.getSuperSubscriberInfo(); if (findState.clazz == superclassInfo.getSubscriberClass()) { // 肯定是所需查找的類 return superclassInfo; } } if (subscriberInfoIndexes != null) { // 從咱們傳進來的subscriberInfoIndexes中獲取相應的訂閱者信息 for (SubscriberInfoIndex index : subscriberInfoIndexes) { SubscriberInfo info = index.getSubscriberInfo(findState.clazz); if (info != null) { return info; } } } return null; }
可見就在這個方法裏面,應用到了咱們生成的索引,避免咱們須要在findSubscriberMethods時去調用耗時的findUsingReflection方法。在實際使用中,Nexus6上一個Activity註冊EventBus須要10毫秒左右,而使用了索引後能下降到3毫秒左右,效果很是明顯。雖然這個索引的實現邏輯有點繞,並且還存在一些坑(好比後面講到的混淆問題),但實現的手段很是巧妙,尤爲是「把耗時的操做在編譯的時候完成」和「用對象池減小建立對象的性能開銷」的思想值得咱們開發者借鑑。
混淆做爲版本發佈必備的流程,常常會鬧出不少奇奇怪怪的問題,且不方便定位,尤爲是EventBus這種依賴反射技術的庫。一般狀況下都會把相關的類和回調方法都keep住,但這樣其實會留下被人反編譯後破解的後顧之憂,因此咱們的目標是keep最少的代碼。
首先,由於EventBus 3棄用了反射的方式去尋找回調方法,改用註解的方式。做者的意思是在混淆時就不用再keep住相應的類和方法。可是咱們在運行時,卻會報java.lang.NoSuchFieldError: No static field POSTING
。網上給出的解決辦法是keep住全部eventbus相關的代碼:
-keep class de.greenrobot.** {*;}
其實咱們仔細分析,能夠看到是由於在SubscriberMethodFinder的findUsingReflection方法中,在調用Method.getAnnotation()時獲取ThreadMode這個enum失敗了,因此咱們只須要keep住這個enum就能夠了(以下)。
-keep public enum org.greenrobot.eventbus.ThreadMode { public static *; }
這樣就能正常編譯經過了,但若是使用了索引加速,是不會有上面這個問題的。由於在找方法時,調用的不是findUsingReflection,而是findUsingInfo。可是使用了索引加速後,編譯後卻會報新的錯誤:Could not find subscriber method in XXX Class. Maybe a missing ProGuard rule?
這就很好理解了,由於生成索引GeneratedSubscriberIndex是在代碼混淆以前進行的,混淆以後類名和方法名都不同了(上面這個錯誤是方法沒法找到),得keep住全部被Subscribe註解標註的方法:
-keepclassmembers class * { @de.greenrobot.event.Subscribe <methods>; }
因此又倒退回了EventBus2.4時不能混淆onEvent開頭的方法同樣的處境了。因此這裏就得權衡一下利弊:使用了註解不用索引加速,則只須要keep住EventBus相關的代碼,現有的代碼能夠正常的進行混淆。而使用了索引加速的話,則須要keep住相關的方法和類。
目前EventBus只支持跨線程,而不支持跨進程。若是一個app的service起到了另外一個進程中,那麼註冊監聽的模塊則會收不到另外一個進程的EventBus發出的事件。這裏能夠考慮利用IPC作映射表,並在兩個進程中各維護一個EventBus,不過這樣就要本身去維護register和unregister的關係,比較繁瑣,並且這種狀況下一般用廣播會更加方便,你們能夠思考一下有沒有更優的解決方案。
在使用EventBus時,一般咱們會把兩個模塊相互監聽,來達到一個相互回調通訊的目的。但這樣一旦出現死循環,並且若是沒有相應的日誌信息,很難定位問題。因此在使用EventBus的模塊,若是在回調上有環路,並且回調方法複雜到了必定程度的話,就要考慮把接收事件專門封裝成一個子模塊,同時考慮避免出現事件環路。
固然,EventBus並非重構代碼的惟一之選。做爲觀察者模式的「同門師兄弟」——RxJava,做爲功能更爲強大的響應式編程框架,能夠輕鬆實現EventBus的事件總線功能(RxBus)。但畢竟大型項目要接入RxJava的成本高,複雜的操做符須要開發者投入更多的時間去學習。因此想在成熟的項目中快速地重構、解耦模塊,EventBus依舊是咱們的不二之選。
本文總結了EventBus 3的使用方法,運行原理和一些新特性,讓你們能直觀地看到這個組件的優勢和缺點,同時讓你們在考慮是否在項目中引入EventBus時內心有個底。最後感謝Markus Junginger大神開源瞭如此實用的組件,以及組內同事在筆者探究EventBus原理時提供的幫助,但願你們在看完本文後都能有所收穫,成爲NB的Android開發老司機。
更多精彩內容歡迎關注Bugly的微信公衆帳號:
騰訊 Bugly 是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧…