基于觀察者模式的事件發布/訂閱框架。通過極少的代碼實現模塊間的通信,無須層層傳遞。使用方便,性能高,接入成本低,降低耦合,支持多線程的優點。
流程圖在EventBus 3.0版本中引入了 EventBusAnnotationPRocessor(注解分析生成索引)技術,大大提高了EventBus的運行效率。
流程圖App 的 build.gradle 中:
compile'org.greenrobot:eventbus:3.0.0'首先,在項目gradle的dependencies中引入apt編譯插件:
classpath'com.neenbedankt.gradle.plugins:android-apt:1.8'然后,在App的build.gradle中應用apt插件,并設置apt生成的索引的包名和類名:
applyplugin:'com.neenbedankt.android-apt'apt { arguments{ eventBusIndex"com.study.sangerzhong.studyapp.MyEventBusIndex" }}接著,在App的dependencies中引入EventBusAnnotationProcessor:
apt'org.greenrobot:eventbus-annotation-processor:3.0.1'注意:
注解解析依賴于android-apt-plugin。加速索引可以不加應用EventBusAnnotationProcessor卻沒有設置arguments,編譯時會報錯:No option eventBusIndex passed to annotation processor此時需要先編譯一次,生成索引類。編譯成功之后,會在/ProjectName/app/build/generated/source/apt/PakageName/下看到通過注解分析生成的索引類,這樣便可以在初始化EventBus時應用生成的索引了。1.3 初始化
兩種初始方式:
默認單例獲取(EventBus默認有一個單例)EventBus mEventBus = EventBus.getDefault();自定義//如:應用生成好的索引時EventBus mEventBus = EventBus.builder() .addIndex(new MyEventBusIndex()) .build();//如:自定義的設置應用到默認單利中EventBus mEventBus = EventBus.builder() .addIndex(newMyEventBusIndex()) .installDefaultEventBus();1.4 定義事件
所有能被實例化為 Object 的實例都可以作為事件:
public class DriverEvent { public String info; }注意:如果在EventBus 3.0中使用了索引加速,事件類的修飾符必須為public,不然編譯時會報錯:Subscriber method must be public。
1.5 監聽事件
在訂閱事件(接收事件)的模塊,注冊EventBus:
//如:Activity 中可寫在 onCreate() 方法內mEventBus.register(Object);在訂閱事件(接收事件)的模塊,注銷EventBus:
//如:Activity 中可寫在 onDestory() 方法內mEventBus.unregister(Object);3.0之前,需要區分是否監聽黏性(sticky)事件。3.0中,改為添加注解的形式:
@Subscribe(threadMode = ThreadMode.POSTING, priority =0, sticky =true)public void handleEvent ( DriverEventevent ){ Log.d(TAG,event.info);}注解有三個參數:threadMode: 回調所在的線程priority: 優先級sticky: 是否接收黏性事件注冊了監聽的模塊必須有一個標注Subscribe注解方法,不然在register時會拋出異常:Subscriberclass XXX and its super classes havenopublic methods with the@Subscribeannotation
1.6 發送事件
調用post或postSticky即可:無須注冊
EventBus.getDefault().post(new DriverEvent("magnet:?xt=urn:btih……"));以上便完成了使用EventBus的學習。實際使用中,register 和 unregister 通常與 Activity 和 Fragment 的生命周期相關,ThreadMode.MainThread 解決了界面刷新必須在UI線程的問題,黏性事件可以解決 post 與 register 同時執行時的異步問題,事件傳遞沒有序列化與反序列化的性能消耗。
2. 原理分析
2.1 和新架構
機制
訂閱者模塊需要通過EventBus訂閱相關的事件,并準備好處理事件的回調方法,而事件發布者則在適當的時機把事件post出去,EventBus就能搞定一切。在架構方面,EventBus 3.0與之前稍老版本有不同,如圖:
架構
核心類EventBus,其中subscriptionByEventType是以事件的類為key,訂閱者的回調方法為value的映射關系表。也就是說EventBus在收到一個事件時,可以根據這個事件的類型,在subscriptionByEventType中找到所有監聽了該事件的訂閱者及處理事件的回調方法。而typesBySubscriber則是每個訂閱者所監聽的事件類型表,在取消注冊時可以通過該表中保存的信息,快速刪除subscriptionByEventType中訂閱者的注冊信息,避免遍歷查找。注冊事件、發送事件和注銷都是圍繞著這兩個核心數據結構來展開。上面的Subscription可以理解為每個訂閱者與回調方法的關系,在其他模塊發送事件時,就會通過這個關系,讓訂閱者執行回調方法。
回調方法在這里被封裝成了SubscriptionMethod,里面保存了在需要反射invoke方法時的各種參數,包括優先級,是否接收黏性事件和所在線程等信息。而要生成這些封裝好的方法,則需要SubscriberMethodFinder,它可以在regster時得到訂閱者的所有回調方法,并封裝返回給EventBus。而右邊的加速器模塊,就是為了提高SubscriberMethodFinder的效率。
四個Poster,是EventBus能在不同的線程執行回調方法的核心:POSTING:(調用post所在的線程執行回調)不需要poster來調度,直接運行。MAIN:(UI線程回調)如果post所在線程為UI線程則直接執行,否則通過mainThreadPoster來調度。BACKGROUND:(Backgroud線程回調):如果post所在線程為非UI線程則直接執行,否則通過backgroundPoster來調度。ASYNC:(交給線程池來管理):直接通過asyncPoster調度。不同的Poster會在post事件時,調度相應的事件隊列PendingPostQueue,讓每個訂閱者的回調方法收到相應的事件,并在其注冊的Thread中運行。而這個事件隊列是一個鏈表,由一個個PendingPost組成,其中包含了事件,事件訂閱者,回調方法這三個核心參數,以及需要執行的下一個PendingPost。
2.2 register
根據訂閱者的類來找回調方法,把訂閱者和回調方法封裝成關系,并保存到相應的數據結構中,為隨后的事件分發做好準備,最后處理黏性事件:
register
EventBus 3.0使用了注解表示回調,可以出現相同的ThreadMode的回調方法監聽相同的事件,此時會根據注冊的先后順序,先注冊先分發事件,注意不是先收到事件,收到事件的順序還是得看poster中Handler的調度。
2.3 post
分析事件后,得到所有監聽該事件的訂閱者的回調方法,并利用反射來invoke方法,實現回調:
post
圖中看到poster的調度事件功能,同時調度的單位細化成了Subscription,即每一個方法都有自己的優先級和是否接收黏性事件。在源代碼中為了保證post執行不會出現死鎖,等待和對同一訂閱者發送相同的事件,增加了很多線程保護鎖和標志位,值得每個開發者學習。
2.4 unregister
把在注冊時往兩個數據結構中添加的訂閱者信息刪除即可:
unregister
2.5 黏性事件
為什么要有這個設計呢?舉個栗子:在登陸成功后自動播放歌曲,而登陸和監聽登陸是同時進行的。如果登陸流程走得特別快,在登陸成功后播放模塊才注冊了監聽。此時播放模塊便會錯過了【登陸成功】的事件,出現“雖然登陸成功了,回調卻沒執行”的情況。而如果【登陸成功】這個事件是一個黏性事件的話,那么即使我后來才注冊了監聽(并且回調方法設置為監聽黏性事件),則回調就能在注冊的那一刻被執行,這個問題就能被優雅地解決,而不需要額外去定義其他標志位。
3 索引加速
在EventBus 3.0的介紹中,作者提到以前的版本為了保證性能,在遍歷尋找訂閱者的回調方法時使用反射而不是注解。但現在卻能在使用注解的前提下,大幅度提高性能,同時作者在博客中放出了這張對比圖:
速度對比
性能方面,EventBus 3.0由于使用了注解,比起使用反射來遍歷方法的2.4版本遜色不少。但開啟索引后性能遠遠超出之前的版本。
關于索引加速的具體分析請看原文。
4 其他
4.1 混淆
EventBus 3.0使用注解的方式。作者的意思是在混淆時就不用再keep住相應的類和方法。
//運行時,拋出錯誤java.lang.NoSuchFieldError: No static field POSTING。//解決方法:keep住所有eventbus相關的代碼-keepclassde.greenrobot.** {*;}分析,因為在SubscriberMethodFinder的findUsingReflection方法中,在調用Method.getAnnotation()時獲取ThreadMode這個enum失敗了,所以只需要keep住這個enum就可以了(如下)。
-keeppublicenumorg.greenrobot.eventbus.ThreadMode { publicstatic*; }這樣就能編譯通過了,如果使用了索引加速,是不會有上面這個問題的。因為在找方法時,調用的不是findUsingReflection,而是findUsingInfo。
//使用索引加速后,編譯后拋出錯誤:Could not find subscriber method in XXX Class. Maybe a missing ProGuard rule?因為生成索引GeneratedSubscriberIndex是在代碼混淆之前進行的,混淆之后類名和方法名都不一樣了(上面這個錯誤是方法無法找到),得keep住所有被Subscribe注解標注的方法:
-keepclassmembersclass* { @de.greenrobot.event.Subscribe ;}這里就得權衡一下利弊:使用了注解不用索引加速,則只需要keep住EventBus相關的代碼,現有的代碼可以正常的進行混淆。而使用了索引加速的話,則需要keep住相關的方法和類。
4.2 跨進程
目前EventBus只支持跨線程,而不支持跨進程。如果一個app的service起到了另一個進程中,那么注冊監聽的模塊則會收不到另一個進程的EventBus發出的事件。這里可以考慮利用IPC做映射表,并在兩個進程中各維護一個EventBus,不過這樣就要自己去維護register和unregister的關系,比較繁瑣,而且這種情況下通常用廣播會更加方便,大家可以思考一下有沒有更優的解決方案。
4.3 事件環路
在使用EventBus時,通常會把兩個模塊相互監聽,來達到一個相互回調通信的目的。但這樣一旦出現死循環,而且如果沒有相應的日志信息,很難定位問題。所以在使用EventBus的模塊,如果在回調上有環路,而且回調方法復雜到了一定程度的話,就要考慮把接收事件專門封裝成一個子模塊,同時考慮避免出現事件環路。
5 寫在最后
EventBus并不是重構代碼的唯一之選。作為觀察者模式的“同門師兄弟”——RxJava,作為功能更為強大的響應式編程框架,可以輕松實現EventBus的事件總線功能(RxBus)。但畢竟大型項目要接入RxJava的成本高,復雜的操作符需要開發者投入更多的時間去學習。所以想在成熟的項目中快速地重構、解耦模塊,EventBus依舊是不二之選。
老司機發車了
新聞熱點
疑難解答