1. 程式人生 > >EventBus原始碼詳解(二):進階使用

EventBus原始碼詳解(二):進階使用

寫在前面

EventBus是一個Android平臺上基於事件釋出和訂閱的輕量級框架,可以對釋出者和訂閱者解耦,並簡化Android的事件傳遞。

本文是關於EventBus系列文章的第二篇,相關文章有:

如果你對EventBus不瞭解,我建議先閱讀該系列文章的第一篇,如果對EventBus已略有所知,那麼就可以開始閱讀這文章。這文章是關於EventBus的進階使用,會涉及到粘性事件、EventBus的執行緒排程、EventBus生成索引提高post效率等。

正文

開始介紹EventBus進階使用前,我們先約定幾個概念:

  • 訂閱者:指的是呼叫了EventBus#register(Object)
    的類
  • 訂閱方法:用@Subscribe標記的方法
  • 事件:被EventBus post的類,即傳入EventBus#post(event)event引數

下面開始介紹EventBus的進階使用。

粘性事件

粘性事件有點類似於Android系統的粘性廣播,即在註冊廣播前,就把廣播發送出去,當廣播一註冊時,就會接收到該粘性廣播。而粘性事件也一樣,在訂閱者註冊前,先把粘性事件傳送出去,當訂閱者註冊後,立即觸發粘性事件。

訂閱黏性事件也很簡單,首先也是先定義一個事件(注意:事件無粘性非粘性之分,它們都是類而已,區分粘性是在訂閱方法的宣告中):

public class StickyEvent {
    String args;
    StickyEvent(String args) {
        this
.args = args; } }

然後定義一個訂閱方法,在註解裡用@Subscribe(sticky = true)宣告為粘性事件:

@Subscribe(sticky = true) // 粘性事件
public void onStickyEvent(StickyEvent event) {
    Log.d("Test", event.args);
}

然後在註冊訂閱者前,先post粘性事件:

EventBus.getDefault().postSticky(new StickyEvent("sticky event 1")); // 注意,是呼叫“postSticky”而不是“post”
EventBus.getDefault().register(this); // 一註冊訂閱者,就會呼叫上面的訂閱方法

但需要注意的是,*同一個粘性事件只會快取最近一個,即當你在註冊訂閱者前,多次呼叫postSticky*,只有最後一次呼叫才會被保留:

EventBus.getDefault().postSticky(new StickyEvent("sticky event 1"));  // 被覆蓋
EventBus.getDefault().postSticky(new StickyEvent("sticky event 2"));  // 被覆蓋
EventBus.getDefault().postSticky(new StickyEvent("sticky event 3"));  // 快取
EventBus.getDefault().register(this); // 一註冊訂閱者,只會觸發最後一個粘性事件

開啟logcat會顯示:

sticky event 3

為什麼粘性事件在註冊時就能觸發呢?其實原理很簡單,粘性事件只是會在註冊訂閱者時會被檢測,如果檢測到該訂閱者有訂閱了粘性事件,即立刻呼叫post粘性事件。至於更細一步分析會在解讀原始碼文章裡講,這裡先記著。

可能有人會問,粘性事件怎麼用呢?下面就舉一個用粘性事件來替代Intent在Activity傳輸資料的例子。
首先在MainActivity跳轉到StickyActivity前,先post粘性事件:

/* #MainActivity */
public void startStickyActivity(View view) {
    EventBus.getDefault().postSticky(new StickyEvent("I am args"));
    startActivity(new Intent(this, StickyActivity.class));
}

然後在StickyActivityonCreateonStart裡註冊,就能獲取到MainActivity傳遞的引數:

public class StickyActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sticky);
        EventBus.getDefault().register(this);
    }

    @Subscribe(sticky = true) // 粘性事件
    public void onStickyEvent(StickyEvent event) {
        Log.d("EventBus", event.args);
        init(event.args);
    }

    private void init(String args) {
        // do init
    }
}

粘性事件介紹完了,但還要提醒注意的是:粘性事件可以當作普通事件使用。即呼叫EventBus#post(event)時,所訂閱的方法也會被觸發呼叫。因為EventBus沒有在EventBus#post方法裡對粘性事件進行過濾,而EventBus#postSticky實際上也是呼叫EventBus#post方法,可以先看看原始碼:

/* #EventBus */
public void postSticky(Object event) {
    synchronized (stickyEvents) {
        stickyEvents.put(event.getClass(), event); // stickyEvents是Map,用作快取粘性事件
    }
    // Should be posted after it is putted, in case the subscriber wants to remove immediately
    post(event);
}
訂閱方法優先順序

優先順序是對於同一個訂閱者所訂閱的同一個事件類的不同方法而言的。在宣告訂閱方法時,用@Subscribe(priority = ?)來指定訂閱方法執行的優先順序,預設優先順序為0,優先順序越高,越早被執行。我們來看看下面的例子:

public class PriorityActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_priority);
        EventBus.getDefault().register(this);
        EventBus.getDefault().post(new PriorityEvent());
    }

    @Subscribe(priority = 0) // 指定優先順序為0,預設是0
    public void onLowPriorityEvent(PriorityEvent event) {
        Log.i("TEST", "onLowPriorityEvent");
    }

    @Subscribe(priority = 10) // 指定優先順序為10
    public void onHighPriorityEvent(PriorityEvent event) {
        Log.i("TEST", "onHighPriorityEvent");
    }
}

上面例子會在logcat依次列印:

I/TEST: onHighPriorityEvent
I/TEST: onLowPriorityEvent

EventBus的執行緒排程

執行緒排程指的是可以把訂閱方法拋到所指定的執行緒執行。經過上面的介紹,你可能猜到怎麼聲明瞭?沒錯,也是在標記註解時宣告:

@Subscribe(threadMode = ?) // 宣告執行執行緒
public void onThreadEvent(ThreadEvent event) {
}

ThreadMode有四種模式,分別為:

  • POSTING:在當前呼叫EventBus#post(event)的執行緒執行
  • MAIN:在主執行緒(即Android的UI執行緒)執行
  • BACKGROUND:當前執行執行緒為主執行緒,就切換到後臺執行緒執行;當前執行緒為非主執行緒,則就在當前執行緒執行
  • ASYNC:新開後臺執行緒執行

預設的ThreadModePOSTING

再來看看下面的例子:

public class ChangeThreadActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_change_thread);
        EventBus.getDefault().register(this);
        EventBus.getDefault().post(new ThreadEvent());
    }

    @Subscribe(threadMode = ThreadMode.POSTING)
    public void onPostingThreadEvent(ThreadEvent event) {
        Log.i("TEST", "POSTING --> I am on Thread " + Thread.currentThread().getName());
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMainThreadEvent(ThreadEvent event) {
        Log.i("TEST", "MAIN --> I am on Thread " + Thread.currentThread().getName());
    }

    @Subscribe(threadMode = ThreadMode.BACKGROUND)
    public void onBackgroundThreadEvent(ThreadEvent event) {
        Log.i("TEST", "BACKGROUND --> I am on Thread " + Thread.currentThread().getName());
    }

    @Subscribe(threadMode = ThreadMode.ASYNC)
    public void onAsyncThreadEvent(ThreadEvent event) {
        Log.i("TEST", "ASYNC --> I am on Thread " + Thread.currentThread().getName());
    }
}

執行開啟logcat可以看到下面的日誌:

I/TEST: MAIN –> I am on Thread main
I/TEST: ASYNC –> I am on Thread pool-1-thread-1
I/TEST: POSTING –> I am on Thread main
I/TEST: BACKGROUND –> I am on Thread pool-1-thread-2

嗯,切換執行緒就這麼簡單!你應該知道在訂閱方法執行耗時任務該怎麼做了,就不再舉例了。

EventBus索引生成

有人看到這裡可能一頭霧水,索引是什麼鬼?之前都沒介紹過索引相關的知識。別急,聽我慢慢道來~

EventBus 3.0之前的版本是沒有索引的,檢索訂閱方法是通過反射獲取的。我們都知道反射的效率令人堪憂,如果頻繁地呼叫的話,肯定會對程式的效能造成影響。而greenrobot也意識到這個問題,所以在EventBus 3.0版本新增一個索引的功能,它主要是通過在編譯期處理,生成訂閱者和訂閱方法的對應關係並快取起來,從而在程式執行時能快速索引。

EventBus是運用了觀察者模式,我們知道,觀察者模式一般有兩個階段:準備階段和執行階段。準備階段是維護目標(Subject)和觀察者(Observer)的關係,而索引的生成就是在準備階段。

因為生成索引是在編譯期的,所以需要新增一些配置。首先在project下build.gradle新增:

buildscript {
    dependencies {
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

然後在app或lib下的build.gradle新增apt外掛和EventBus apt工具:

apply plugin: 'com.neenbedankt.android-apt'

dependencies {
    apt 'org.greenrobot:eventbus-annotation-processor:3.0.1'
}

最後,也是在app或lib下的build.gradle apt引數:

apt {
    arguments {
        eventBusIndex "com.leo.eventbus.sample.SampleBusIndex" // 生成索引類,包名和類名可自定義
        verbose "true" // 是否列印編譯除錯日誌
    }
}

build.gradle的完整程式碼就不貼了,如果不懂可以從文末下載原始碼參考。

好,現在我們rebuild下專案,可以看到在 ../build/generated/source/apt/debug/com.leo.eventbus.sample/SampleBusIndex 生成了索引。包名和類名就是我們剛剛在build.gradle配置的。

生成索引.png

生成索引後我們還需要手動給EventBus載入,最好在Application里加載:

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        // 載入索引,新增到預設配置的EventBus
        EventBus.builder().addIndex(new SampleBusIndex()).installDefaultEventBus();
    }
}

細心的朋友可能會注意到我上面的註釋:“新增到預設配置的EventBus”,所謂預設配置的EventBus也就是呼叫EventBus.getDefault()獲取到的,是EventBus提供的,我前面的例子都是用這個預設的。而EventBus例項是可以建立多個並且相互獨立的,這些將在下一節介紹。

至於EventBus.getDefault()相當於我們用單例模式的getInstance,可以先來看看原始碼:

/* EventBus */
static volatile EventBus defaultInstance;

public static EventBus getDefault() {
    if (defaultInstance == null) {
        synchronized (EventBus.class) {
            if (defaultInstance == null) {
                defaultInstance = new EventBus();
            }
        }
    }
    return defaultInstance;
}

installDefaultEventBus就是給defaultInstance初始化,並且規定了不能多次初始化:

public EventBus installDefaultEventBus() {
    synchronized (EventBus.class) {
        if (EventBus.defaultInstance != null) {
            throw new EventBusException("Default instance already exists." +
                    " It may be only set once before it's used the first time to ensure consistent behavior.");
        }
        EventBus.defaultInstance = build();
        return EventBus.defaultInstance;
    }
}

再來看下生成的索引類,它用Map維護著訂閱者和事件的關係,如下(可以先不理解,這部分會在介紹註解時講解,這裡先看下):

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

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

        putIndex(new SimpleSubscriberInfo(com.leo.eventbus.sample2.PriorityActivity.class, true,
                new SubscriberMethodInfo[] {
            new SubscriberMethodInfo("onLowPriorityEvent", com.leo.eventbus.sample2.PriorityEvent.class),
            new SubscriberMethodInfo("onHighPriorityEvent", com.leo.eventbus.sample2.PriorityEvent.class,
                    ThreadMode.POSTING, 10, false),
        }));

        putIndex(new SimpleSubscriberInfo(com.leo.eventbus.sample2.StickyActivity.class, true,
                new SubscriberMethodInfo[] {
            new SubscriberMethodInfo("onStickyEvent", com.leo.eventbus.sample2.StickyEvent.class, ThreadMode.POSTING, 0,
                    true),
        }));

        putIndex(new SimpleSubscriberInfo(com.leo.eventbus.sample2.ChangeThreadActivity.class, true,
                new SubscriberMethodInfo[] {
            new SubscriberMethodInfo("onPostingThreadEvent",
                    com.leo.eventbus.sample2.ChangeThreadActivity.ThreadEvent.class),
            new SubscriberMethodInfo("onMainThreadEvent",
                    com.leo.eventbus.sample2.ChangeThreadActivity.ThreadEvent.class, ThreadMode.MAIN),
            new SubscriberMethodInfo("onBackgroundThreadEvent",
                    com.leo.eventbus.sample2.ChangeThreadActivity.ThreadEvent.class, ThreadMode.BACKGROUND),
            new SubscriberMethodInfo("onAsyncThreadEvent",
                    com.leo.eventbus.sample2.ChangeThreadActivity.ThreadEvent.class, ThreadMode.ASYNC),
        }));

    }

    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;
        }
    }
}

還記得我在上一篇文章 EventBus原始碼詳解(一):基本使用 提到過:事件類和訂閱者類最好用public修飾,否則有可能在生成索引時失敗嗎?這裡提到了可能,那如果不用public修飾,什麼情況下會失敗?什麼情況下不會失敗呢?答案是:當索引類的包名與事件類和訂閱者類的包名相同時,非public修飾但是為包訪問許可權下能生成索引,否則就會失敗。

這稍微想想也知道,如果事件類和訂閱者類是非public或者是private/protected修飾的話,在不同包名下是無法訪問的,所以生成索引會失敗。下面我們來試下,我把之前的介紹執行緒排程的事件類ThreadEvent改成包訪問許可權的:

class ThreadEvent {}

而剛剛我在build.gradle配置索引的包名是:com.leo.eventbus.sample,而我的demo包名是:com.leo.eventbus.sample2,我們重新rebuild下專案,回出現一個錯誤提示,開啟編譯日誌如下:

注: Processing round 1, new annotations: true, processingOver: false
D:\ASWorkspace\EventBusSample\sample2\src\main\java\com\leo\eventbus\sample2\ChangeThreadActivity.java:33: 注: Falling back to reflection because event type is not public
    public void onPostingThreadEvent(ThreadEvent event) {
                                                 ^
注: Indexed @Subscribe at PriorityActivity.onLowPriorityEvent(PriorityEvent)
注: Indexed @Subscribe at PriorityActivity.onHighPriorityEvent(PriorityEvent)
注: Indexed @Subscribe at StickyActivity.onStickyEvent(StickyEvent)
注: Processing round 2, new annotations: false, processingOver: false
注: Processing round 3, new annotations: false, processingOver: true

日誌說的很清楚,事件類為非public修飾,返回使用反射的方式檢索。需要提醒一下的是:索引是維護了訂閱者和訂閱方法(包含事件類)的關係,如果某一個訂閱者或事件類在外部無法訪問,那麼該訂閱者和其全部訂閱方法都不會生成索引。例如訂閱者有兩個訂閱方法,引數分別為事件類Event1public修飾)和Event2(非public修飾),在生成訂閱者和Event2的索引時會失敗,那麼訂閱者和Event1的索引也會拋棄,也就相當於有原子性。

索引的使用也不難,只要注意下我提到的注意事項就可以了,至於生成索引的細節,將會在介紹EventBus註解時講解。

寫在最後

先介紹到這裡了,本來打算把高階配置也寫完的,發現篇幅太長了,所以留在下一篇講~

demo