EventBus源碼解析(一)關于用法和注解

文章基于EventBus 3.0講解。
首先對于EventBus的使用上,大多數人還是比較熟悉的。如果你還每次煩于使用接口回調,廣播去更新數據,那么EventBus可以幫助你解決這個問題。
第一篇主要將一些用法和注解

EventBus源碼解析系列

EventBus源碼解析(一)關于用法和注解
EventBus源碼解析(二)register與unregister
EventBus源碼解析(三)Post方法和注解處理器

一. 使用

先從平常我們的使用方法看,Service處理數據,處理完發給Activity做顯示

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv = (TextView)findViewById(R.id.tv);
        EventBus.getDefault().register(this);
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                Intent intent = new Intent(MainActivity.this , TestService.class);
                startService(intent);
            }
        } , 3000);
        Log.e(TAG, "onCreate: " );
    }

    @Subscribe
    public void onEventMainThread(String s){
        Log.e(TAG, "onEventMainThread: " + s );
        tv.setText(s);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        EventBus.getDefault().unregister(this);
    }

在onCreate調用了EventBus.getDefault().register(this);,在onDestory解除了注冊。注冊之后就可以在@Subcribe注解下面的方法接收到。而發送事件則是在Service

 @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        EventBus.getDefault().post("Service Test");
        return super.onStartCommand(intent, flags, startId);
    }

Service和Activity之間的通訊通過EventBus就可以很簡單的實現出了,如果換成以前的話,則是使用

  • 接口回調
  • IBinder
  • 廣播

來進行聯系,但是如果這樣的話一些代碼上就會顯得有些冗余,而使用EventBus則可以很簡便。

此外,這EventBus的版本迭代上,在3.0則是采用了注解來進行監聽事件。
上面那個監聽方法

@Subscribe
    public void onEventMainThread(String s){
        Log.e(TAG, "onEventMainThread: " + s );
        tv.setText(s);
    }

只要通過@Subcribe注解的方法,且類型是public的,方法名可以自由取

@Subscribe
    public void test(String s){
        Log.e(TAG, "onEventMainThread: " + s );
        tv.setText(s);
    }

而在3.0之前則默認了onEvent+類型 的方法名,然后通過反射來獲取對應的方法。

    //3.0前的版本
    public void onEventMainThread(String s){
        Log.e(TAG, "onEventMainThread: " + s );
        tv.setText(s);
    }

二.注解

這里主要說說注解的使用與實例。沒知道注解的可以了解的。
前面講到了EventBus的一個注解@Subscribe,點進去看下類型

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Subscribe {
    ThreadMode threadMode() default ThreadMode.POSTING;
    ...
    boolean sticky() default false;
    ...
    int priority() default 0;
}

沒了解過注解的話可能會有點懵。

注解的語法其實比較簡單,除了@符號的使用外,它基本與Java固有的語法一樣。Java SE5則內置了三種標準的注解

  • @Override:表示當前的方法將覆蓋超類中的方法
  • @Deprecated:使用了注解為它的元素編譯器將發出警告,因為@Deprecated注解是被棄用的代碼,不被贊成
  • @SuppressWarnings:關閉編譯器警告信息

對于上述的三個注解大家在日常開發中算是經常見到的,點進去看他們的結構也和@Subscribe差不多.

Java提供了4種注解
  • @Target :表示該注解可以用于什么地方,可能的ElementType參數有:

  • @Retention:表明需要在什么級別保存該注解信息。可選的RetentionPolicy參數包括:

    • SOURCE:注解將被編譯器丟棄
    • CLASS:注解在class文件中可用,但會被VM丟棄
    • RUNTIME:VM將在運行期間保留注解,因此可以通過反射機制讀取注解的信息。
  • @Document:將注解包含在Javadoc中

  • @Inherited:允許子類繼承父類中的注解

定義一個注解的方法

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
}

除了@符號,注解很像是一個接口。定義注解的時候需要用到元注解,上面用到了@Target@RetentionPolicy,它們的含義在上面已給出。

在注解中一般會有一些元素來表示某些值,注解里面的元素看起來很像接口的方法,唯一區別在于注解可以為其制定默認值,沒有元素的注解稱為標記注解,上面的Test就是一個標記注解。

注解的可用的類型包括以下幾種:所有基本類型、String、Class、enum、Annotation、以上類型的數組形式。元素不能有不確定的值,即要么有默認值,要么在使用注解的時候提供元素的值。而且元素不能使用null作為默認值。注解在只有一個元素且該元素的名稱是value的情況下,在使用注解的時候可以省略“value=”,直接寫需要的值即可。

比如

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
    public String id();
    public String userName() default "Hohohong";
}

這里則有兩個元素,他們的類型是String型,第二個元素則設置了默認值。再看下前面@Subscrive注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Subscribe {
    ThreadMode threadMode() default ThreadMode.POSTING;
    ...
    boolean sticky() default false;
    ...
    int priority() default 0;
}

它保留在VM運行時,用于方法上。三個元素中,第一個元素的類型是Enum型,默認值則為ThreadMode.POSTING,也比較好理解了。

定義好注解之后,就是如何使用了

public class TestUtil {
    @Test(id="0" , userName = "Hong")
    public void getData(String data){
        Log.e("TestUtil", "getData: " + data );
    }

    @Test(id="1")
    public void dataTest(){
        Log.e("TestUtil", "dataTest" );
    }

使用注解最主要的部分在于對注解的處理,那么就會涉及到注解處理器。

從原理上講,注解處理器就是通過反射機制獲取被檢查方法上的注解信息,然后根據注解元素的值進行特定的處理。

 public static void main(String args[]){
        trackTest(TestUtil.class);
    }
    public static void trackTest(Class<?> cls){
        Method[] methods = cls.getMethods();
        for(Method method : methods){
            Test test = method.getAnnotation(Test.class);
            if(test != null){
                System.out.println("Found Test : id = " + test.id() + "  userName = " + test.userName() );
            }
        }
    }

這里就可以拿到每個注解方法上注解的一些屬性。
關于注解更多的使用就不擴展開了。

三.EventBus的注解

還是前面那個@Subscribe

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Subscribe {
    ThreadMode threadMode() default ThreadMode.POSTING;
    ...
    boolean sticky() default false;
    ...
    int priority() default 0;
}

ThreadMode是一個enum類型,里面定義了四種類型

public enum ThreadMode {
   
    POSTING,

    MAIN,

    BACKGROUND,

    ASYNC
}

對應的方法則為

  • onEventPostThread:代表這個方法會在當前發布事件的線程執行,也就是執行事件和發送事件都在同一個線程中。(默認是這個)

  • onEventMainThread:代表這個方法會在主線程執行

  • onEventBackgroundThread:如果發送事件的線程不是UI線程,則運行在該線程中。如果發送事件的是UI線程,則它運行在由EventBus維護的一個單獨的線程池中,事件會加入后臺任務隊列,使用線程池一個接一個調用。

  • onEventAsync:運行在單獨的工作線程中,不論發送事件的線程是否為主線程。跟BackgroundThread不一樣,該模式的所有線程是獨立的,因此適用于長耗時操作,例如網絡訪問。它也是使用線程池調用,注意沒有BackgroundThread中的一個接一個。

3.1ThreadMode.POSTING

首先@Subscribe它里面ThreadMode默認值是POSTING,也就是說,執行事件和發送事件在同一個線程。

@Subscribe
    public void onEventMainThread(String s){
        Log.e(TAG, "Current Thread name: " + Thread.currentThread().getName() );
        Log.e(TAG, "Current Thread is MainThread? : " + (Thread.currentThread() == Looper.getMainLooper().getThread()) );
        tv.setText(s); //主線程才能更新UI,如果POST在子線程運行則報錯
    }
@Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        EventBus.getDefault().post("Service Test");
        return super.onStartCommand(intent, flags, startId);
    }

當然這里的方法名是隨便的,如果發送事件在子線程的話可能會有點誤區。結果為

 E/MainActivity: Current Thread name: main
 E/MainActivity: Current Thread is MainThread? : true

如果在子線程Post

 @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                EventBus.getDefault().post("Service Test");
            }
        } , "childThread").start();
        return super.onStartCommand(intent, flags, startId);
    }

結果則為

E/MainActivity: Current Thread name: childThread
E/MainActivity: Current Thread is MainThread? : false

此時在方法里則不能進行UI更新操作。此時如果把它的模式設置為MAIN,則沒問題

3.2 ThreadMode.MAIN

@Subscribe(threadMode = ThreadMode.MAIN)
    public void onEventMainThread(String s){
        Log.e(TAG, "Current Thread name: " + Thread.currentThread().getName() );
        Log.e(TAG, "Current Thread is MainThread? : " + (Thread.currentThread() == Looper.getMainLooper().getThread()) );
        tv.setText(s);
    }

結果則跟前面一樣,MAIN則是無論發送事件是否在主線程,執行事件總是在主線程

 E/MainActivity: Current Thread name: main
 E/MainActivity: Current Thread is MainThread? : true

3.3 ThreadMode.BACKGROUND

來驗證下BACKGROUND,前面介紹則是如果發送事件的線程不是UI線程,則運行在該線程中。如果發送事件的是UI線程,則它運行在由EventBus維護的一個單獨的線程池中。延續前面的例子,發送事件是在子線程childThread中,執行事件方法我們指定為ThreadMode.BACKGROUND

 @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                EventBus.getDefault().post("Service Test");
            }
        } , "childThread").start();
        return super.onStartCommand(intent, flags, startId);
    }
 @Subscribe(threadMode = ThreadMode.BACKGROUND)
    public void onEventMainThread(String s){
        Log.e(TAG, "Current Thread name: " + Thread.currentThread().getName() );
        Log.e(TAG, "Current Thread is MainThread? : " + (Thread.currentThread() == Looper.getMainLooper().getThread()) );
        tv.setText(s);
    }

此時結果則為

E/MainActivity: Current Thread name: childThread
E/MainActivity: Current Thread is MainThread? : false

跟描述的一樣
我們將發送事件的方法放到UI線程看下

 @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
                EventBus.getDefault().post("Service Test");
        return super.onStartCommand(intent, flags, startId);
    }

結果則為

E/MainActivity: Current Thread name: pool-1-thread-1
E/MainActivity: Current Thread is MainThread? : false

可以看到它運行在EventBus維護的一個線程池中。

3.3 ThreadMode.ASYNC

和前面一樣做測試,就不貼出代碼,直接給出測試結果。
無論當前發送事件是否在主線程還是子線程,執行事件都是在EventBus的線程池中

E/MainActivity: Current Thread name: pool-1-thread-1
E/MainActivity: Current Thread is MainThread? : false

那么從這一點上看又與ThreadMode.BACKGROUND有什么不用呢,前面說到,BACKGROUND里面的線程池會把當前事件添加到任務隊列,一個執行完才到下一個,而ThreadMode.ASYNC從名字上看就知道是異步的,也就是每個事件都可以同時并發執行。

我們看下測試用例
首先,我們一連發送3個事件

 @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        EventBus.getDefault().post("1 Service Test");
        EventBus.getDefault().post("2 Service Test");
        EventBus.getDefault().post("3 Service Test");
        return super.onStartCommand(intent, flags, startId);
    }

模式指定為BACKGROUND

 @Subscribe(threadMode = ThreadMode.BACKGROUND)
    public void onEventMainThread(String s){
        Log.e(TAG, "currrent String : " + s );
        Log.e(TAG, "Current Thread name: " + Thread.currentThread().getName() );
        Log.e(TAG, "Current Thread is MainThread? : " + (Thread.currentThread() == Looper.getMainLooper().getThread()) );
        //tv.setText(s);
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

看下結果輸出

03-12 04:29:43.259 E/MainActivity: currrent String : 1 Service Test
03-12 04:29:43.259 E/MainActivity: Current Thread name: pool-1-thread-1
03-12 04:29:43.259 E/MainActivity: Current Thread is MainThread? : false

03-12 04:29:46.263 E/MainActivity: currrent String : 2 Service Test
03-12 04:29:46.263 E/MainActivity: Current Thread name: pool-1-thread-1
03-12 04:29:46.263 E/MainActivity: Current Thread is MainThread? : false

03-12 04:29:49.263 E/MainActivity: currrent String : 3 Service Test
03-12 04:29:49.263 E/MainActivity: Current Thread name: pool-1-thread-1
03-12 04:29:49.263 E/MainActivity: Current Thread is MainThread? : false

可以看到他們里面用的都是同一個線程,一個事件在執行,另一個事件則需要等待。

再看下指定為ThreadMode.ASYNC的結果

03-12 04:27:43.435 E/MainActivity: currrent String : 1 Service Test
03-12 04:27:43.435 E/MainActivity: Current Thread name: pool-1-thread-1
03-12 04:27:43.435 E/MainActivity: Current Thread is MainThread? : false
    
03-12 04:27:43.435 E/MainActivity: currrent String : 2 Service Test
03-12 04:27:43.435 E/MainActivity: Current Thread name: pool-1-thread-2
03-12 04:27:43.435 E/MainActivity: Current Thread is MainThread? : false
    
03-12 04:27:43.439 E/MainActivity: currrent String : 3 Service Test
03-12 04:27:43.439 E/MainActivity: Current Thread name: pool-1-thread-3
03-12 04:27:43.439 E/MainActivity: Current Thread is MainThread? : false

注意看時間和線程名字,可以看到他們幾乎是同時執行的,而且各自運行在單獨的線程之中。對比出來也比較明了了。

講解完ThreadMode之后,@Subscribe還有其他兩個屬性

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Subscribe {
    ThreadMode threadMode() default ThreadMode.POSTING;
    ...
    boolean sticky() default false; //是否是粘性操作
    ...
    int priority() default 0; //優先級
}
  • int priority:表示的是當前事件的優先級,設置該優先級的目的是,當一個事件有多個訂閱者的時候,優先級高的會優先接收到事件。
 @Subscribe(threadMode = ThreadMode.MAIN ,priority = 0)
    public void priority0(String s){
        Log.e(TAG, "priority0: " + s );
    }

    @Subscribe(threadMode = ThreadMode.MAIN ,priority = 50)
    public void priority50(String s){
        Log.e(TAG, "priority50: " + s );
    }

    @Subscribe(threadMode = ThreadMode.MAIN ,priority = 100 )
    public void priority100(String s){
        Log.e(TAG, "priority100: " + s );
    }

發送一個事件之后,接受到的結果為

03-13 01:46:57.263 E/MainActivity: priority100: Service Test
03-13 01:46:57.263 E/MainActivity: priority50: Service Test
03-13 01:46:57.263 E/MainActivity: priority0: Service Test

如果此時去掉優先級的話,則順序是無序的。

  • boolean sticky:表示的是粘性事件,類似于Android的粘性廣播,通俗講就是當你發送事件的時機比注冊時機早的時候,如果設置了sticky粘性,則可以在發送事件完畢后再注冊,也可以收到事件,但對于同種事件類型(參數類型)的話只會接受到最近發送的粘性事件,以前的不會收到。我們來驗證一下。
   @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        EventBus.getDefault().postSticky("Stick Event 1");
        EventBus.getDefault().postSticky("Stick Event 2");
        EventBus.getDefault().postSticky(10);
        EventBus.getDefault().postSticky(20);
    }
    
    @Subscribe(threadMode = ThreadMode.MAIN , sticky = true)
    public void receiveEventString(String s){
        Log.e(TAG, "receiveEventString: " + s );
    }
    @Subscribe(threadMode = ThreadMode.MAIN , sticky = true)
    public void receiveEventInt(Integer i){
        Log.e(TAG, "receiveEventInt: " + i );
    }

可以看到這里注冊的時機是在按鈕點擊之后,但之前消息已經是發送出去了。此時點擊注冊后,可以看到輸出結果

E/MainActivity: receiveEvent: Stick Event 2
E/MainActivity: receiveEventInt: 20

粘性事件的內部,則是用了一個Map在保存這些粘性事件,而key則是這些粘性事件的參數類型,所以對于同個參數類型的話,最終只會保存最近那個使用的。

 public void postSticky(Object event) {
        synchronized (stickyEvents) {
            stickyEvents.put(event.getClass(), event);
        }
        // Should be posted after it is putted, in case the subscriber wants to remove immediately
        post(event);
    }

粘性事件的發送必須滿足
* 發送事件必須是EventBus.getDefault().postSticky
* 方法的注解要設置stick = true,而這個默認值則為false

四.小結

  • EventBus在3.0版本可說是做了大改,很多之前版本一些缺點都做了改正,提供了注解,性能有了很大的提升,其實是提供了4種ThreadMode來適應不同的網絡情況,相比于Otto來說EventBus的網絡請求功能更加豐富

  • ThreadMode.POST:代表這個方法會在當前發布事件的線程執行,也就是執行事件和發送事件都在同一個線程中。(默認是這個)

  • ThreadMode.MAIN:代表這個方法會在主線程執行

  • ThreadMode.BACKGROUND:如果發送事件的線程不是UI線程,則運行在該線程中。如果發送事件的是UI線程,則它運行在由EventBus維護的一個單獨的線程池中,事件會加入后臺任務隊列,使用線程池一個接一個調用。

  • ThreadMode.ASYNC:運行在單獨的工作線程中,不論發送事件的線程是否為主線程。跟BackgroundThread不一樣,該模式的所有線程是獨立的,因此適用于長耗時操作,例如網絡訪問。它也是使用線程池調用,注意沒有BackgroundThread中的一個接一個。

參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,737評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,699評論 2 370

推薦閱讀更多精彩內容

  • EventBus 是一個Android端優化的 publish/subscribe 消息總線,簡化了應用程序各個組...
    王世軍Steven閱讀 1,864評論 4 21
  • EventBus用法及源碼解析目錄介紹1.EventBus簡介1.1 EventBus的三要素1.2 EventB...
    楊充211閱讀 1,904評論 0 4
  • 對于Android開發老司機來說肯定不會陌生,它是一個基于觀察者模式的事件發布/訂閱框架,開發者可以通過極少的代碼...
    飛揚小米閱讀 1,483評論 0 50
  • 簡介 我們知道,Android應用主要是由4大組件構成。當我們進行組件間通訊時,由于位于不同的組件,通信方式相對麻...
    Whyn閱讀 547評論 0 1
  • 前言:EventBus出來已經有一段時間了,github上面也有很多開源項目中使用了EventBus。所以抽空學習...
    Kerry202閱讀 1,293評論 1 2