EventBus 3.0

對于Android開發老司機來說肯定不會陌生,它是一個基于觀察者模式的事件發布/訂閱框架,開發者可以通過極少的代碼去實現多個模塊之間的通信,而不需要以層層傳遞接口的形式去單獨構建通信橋梁。從而降低因多重回調導致的模塊間強耦合,同時避免產生大量內部類。它擁有使用方便,性能高,接入成本低和支持多線程的優點,實乃模塊解耦、代碼重構必備良藥。

image.png

作為Markus Junginger大神耗時4年打磨、超過1億接入量、Github 9000+ star的明星級組件,分析EventBus的文章早已是數不勝數。在EventBus 3中引入了EventBusAnnotationProcessor(注解分析生成索引)技術,大大提高了EventBus的運行效率。而分析這個加速器的資料在網上很少,因此本文會把重點放在分析這個EventBus 3的新特性上,同時分享一些踩坑經驗,并結合源碼分析及UML圖,以直觀的形式和大家一起學習EventBus 3的用法及運行原理。

使用EventBus

image.png

1.導入組件

// 打開App的build.gradle,在dependencies中添加最新的EventBus依賴:
compile 'org.greenrobot:eventbus:3.0.0'

如果不需要索引加速的話,就可以直接跳到第二步了。而要應用最新的EventBusAnnotationProcessor則比較麻煩,因為注解解析依賴于android-apt-plugin。我們一步一步來,首先在項目gradle的dependencies中引入apt編譯插件:

classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

然后在App的build.gradle中應用apt插件,并設置apt生成的索引的包名和類名:

apply plugin: 'com.neenbedankt.android-apt'
apt {
    arguments {
        eventBusIndex "org.greenrobot.eventbusperf.MyEventBusIndex"
    }
}

接著在App的dependencies中引入EventBusAnnotationProcessor:

apt 'org.greenrobot:eventbus-annotation-processor:3.0.1'
//或者使用源碼
apt project(':EventBusAnnotationProcessor')

此時需要我們先編譯一次,生成索引類。編譯成功之后,就會發現在\ProjectName\app\build\generated\source\apt\PakageName\下看到通過注解分析生成的索引類,這樣我們便可以在初始化EventBus時應用我們生成的索引了。

2.初始化EventBus
EventBus默認有一個單例,可以通過getDefault()獲取,也可以通過EventBus.builder()構造自定義的EventBus,比如要應用我們生成好的索引時:

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

如果想把自定義的設置應用到EventBus默認的單例中,則可以用installDefaultEventBus()方法:

EventBus.builder().addIndex(new MyEventBusIndex()).installDefaultEventBus();

3.定義事件
所有能被實例化為Object的實例都可以作為事件。
在最新版的eventbus 3中如果用到了索引加速,事件類的修飾符必須為public,不然編譯時會報錯:Subscriber method must be public。

4.監聽事件
首先把作為訂閱事件的模塊通過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

5.發送事件
調用post或者postSticky即可:

mEventBus.post(new DriverEvent("magnet:?xt=urn:btih……"));

在實際項目的使用中,register和unregister通常與Activity和Fragment的生命周期相關,ThreadMode.MainThread可以很好地解決Android的界面刷新必須在UI線程的問題,不需要再回調后用Handler中轉(EventBus中已經自動用Handler做了處理),黏性事件可以很好地解決post與register同時執行時的異步問題(這個在原理中會說到),事件的傳遞也沒有序列化與反序列化的性能消耗,足以滿足我們大部分情況下的模塊間通信需求。

EventBus原理分析

image.png

1.訂閱注冊(register)

簡單來說就是:根據訂閱者的類來找回調方法,把訂閱者和回調方法封裝成關系,并保存到相應的數據結構中,為隨后的事件分發做好準備,最后處理黏性事件:

//3.0版本的注冊
EventBus.getDefault().register(this);
       
//2.x版本的注冊
EventBus.getDefault().register(this);
EventBus.getDefault().register(this, 100);
EventBus.getDefault().registerSticky(this, 100);
EventBus.getDefault().registerSticky(this);

可以看到2.x版本中有四種注冊方法,區分了普通注冊和粘性事件注冊,并且在注冊時可以選擇接收事件的優先級,這里我們就不對2.x版本做過多的研究了,如果想研究可以參照此篇文章.由于3.0版本將粘性事件以及訂閱事件的優先級換了一種更好的實現方式,所以3.0版本中的注冊就變得簡單,只有一個register()
方法即可.

public void register(Object subscriber) {
    //首先獲得訂閱者的class對象
    Class<?> subscriberClass = subscriber.getClass();
    //通過subscriberMethodFinder來找到訂閱者訂閱了哪些事件.返回一個SubscriberMethod對象的List,SubscriberMethod
    //里包含了這個方法的Method對象,以及將來響應訂閱是在哪個線程的ThreadMode,以及訂閱的事件類型eventType,以及訂閱的優
    //先級priority,以及是否接收粘性sticky事件的boolean值.
    List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
    synchronized (this) {
        for (SubscriberMethod subscriberMethod : subscriberMethods) {
            //訂閱
            subscribe(subscriber, subscriberMethod);
        }
    }
}

可以看到register()``方法很簡潔,代碼里的注釋也很清楚了,我們可以看出通過subscriberMethodFinder.findSubscriberMethods(subscriberClass)方法就能返回一個SubscriberMethod的對象,而SubscriberMethod里包含了所有我們需要的接下來執行subscribe()的信息.所以我們先去看看findSubscriberMethods()`是怎么實現的,然后我們再去關注subscribe()。

SubscriberMethodFinder的實現

一句話來描述SubscriberMethodFinder類就是用來查找和緩存訂閱者響應函數的信息的類。所以我們首先要知道怎么能獲得訂閱者響應函數的相關信息。在3.0版本中,EventBus提供了一個EventBusAnnotationProcessor注解處理器來在編譯期通過讀取@Subscribe()注解并解析,處理其中所包含的信息,然后生成java類來保存所有訂閱者關于訂閱的信息,這樣就比在運行時使用反射來獲得這些訂閱者的信息速度要快.我們可以參考EventBus項目里的EventBusPerformance這個例子,編譯后我們可以在build文件夾里找到這個類,MyEventBusIndex 類,當然類名是可以自定義的.我們大致看一下生成的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>();

        putIndex(new SimpleSubscriberInfo(org.greenrobot.eventbusperf.testsubject.PerfTestEventBus.SubscriberClassEventBusAsync.class,
                true, new SubscriberMethodInfo[]{
                new SubscriberMethodInfo("onEventAsync", TestEvent.class, ThreadMode.ASYNC),
        }));

        putIndex(new SimpleSubscriberInfo(TestRunnerActivity.class, true, new SubscriberMethodInfo[]{
                new SubscriberMethodInfo("onEventMainThread", TestFinishedEvent.class, ThreadMode.MAIN),
        }));
    }

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

可以看出是使用一個靜態HashMap即:SUBSCRIBER_INDEX來保存訂閱類的信息,其中包括了訂閱類的class對象,是否需要檢查父類,以及訂閱方法的信息SubscriberMethodInfo的數組,SubscriberMethodInfo中又保存了:訂閱方法的方法名,訂閱的事件類型,觸發線程,是否接收sticky事件以及優先級priority.這其中就保存了register()的所有需要的信息,如果再配置EventBus的時候通過EventBusBuilder配置:eventBus = EventBus.builder().addIndex(new MyEventBusIndex()).build();來將編譯生成的MyEventBusIndex配置進去,這樣就能在SubscriberMethodFinder類中直接查找出訂閱類的信息,就不需要再利用注解判斷了(也就是我們說的渦輪引擎),當然這種方法是作為EventBus的可選配置,SubscriberMethodFinder同樣提供了通過注解來獲得訂閱類信息的方法,下面我們就來看findSubscriberMethods()到底是如何實現的:

List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
    //先從METHOD_CACHE取看是否有緩存,key:保存訂閱類的類名,value:保存類中訂閱的方法數據,
    List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
    if (subscriberMethods != null) {
        return subscriberMethods;
    }
    //是否忽略注解器生成的MyEventBusIndex類
    if (ignoreGeneratedIndex) {
        //利用反射來讀取訂閱類中的訂閱方法信息
        subscriberMethods = findUsingReflection(subscriberClass);
    } else {
        //從注解器生成的MyEventBusIndex類中獲得訂閱類的訂閱方法信息
        subscriberMethods = findUsingInfo(subscriberClass);
    }
    if (subscriberMethods.isEmpty()) {
        throw new EventBusException("Subscriber " + subscriberClass
                + " and its super classes have no public methods with the @Subscribe annotation");
    } else {
        //保存進METHOD_CACHE緩存
        METHOD_CACHE.put(subscriberClass, subscriberMethods);
        return subscriberMethods;
    }
}

注釋很詳細我們就不在多說,由于篇幅原因我們就不在分析findUsingInfo()方法,其無非就是通過查找我們上面所說的MyEventBusIndex類中的信息,來轉換成List<SubscriberMethod>從而獲得訂閱類的相關訂閱函數的各種信息.有興趣的可以自己研究看看,下面我們就來看findUsingReflection()方法是如何實現的:

private List<SubscriberMethod> findUsingReflection(Class<?> subscriberClass) {
    //FindState 用來做訂閱方法的校驗和保存
    FindState findState = prepareFindState();
    findState.initForSubscriber(subscriberClass);
    while (findState.clazz != null) {
        //通過反射來獲得訂閱方法信息
        findUsingReflectionInSingleClass(findState);
        //查找父類的訂閱方法
        findState.moveToSuperclass();
    }
    //獲取findState中的SubscriberMethod(也就是訂閱方法List)并返回
    return getMethodsAndRelease(findState);
}

這里通過FindState類來做訂閱方法的校驗和保存,并通過FIND_STATE_POOL靜態數組來保存FindState對象,可以使FindState復用,避免重復創建過多的對象.最終是通過findUsingReflectionInSingleClass()來具體獲得相關訂閱方法的信息的:

private void findUsingReflectionInSingleClass(FindState findState) {
    Method[] methods;
    //通過反射得到方法數組
    try {
        // This is faster than getMethods, especially when subscribers are fat classes like Activities
        methods = findState.clazz.getDeclaredMethods();
    } catch (Throwable th) {
        // Workaround for java.lang.NoClassDefFoundError, see https://github.com/greenrobot/EventBus/issues/149
        methods = findState.clazz.getMethods();
        findState.skipSuperClasses = true;
    }
    //遍歷Method
    for (Method method : methods) {
        int modifiers = method.getModifiers();
        if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
            Class<?>[] parameterTypes = method.getParameterTypes();
            //保證必須只有一個事件參數
            if (parameterTypes.length == 1) {
                //得到注解
                Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
                if (subscribeAnnotation != null) {
                    Class<?> eventType = parameterTypes[0];
                    //校驗是否添加該方法
                    if (findState.checkAdd(method, eventType)) {
                        ThreadMode threadMode = subscribeAnnotation.threadMode();
                        //實例化SubscriberMethod對象并添加
                        findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,
                                subscribeAnnotation.priority(), subscribeAnnotation.sticky()));
                    }
                }
            } else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
                String methodName = method.getDeclaringClass().getName() + "." + method.getName();
                throw new EventBusException("@Subscribe method " + methodName +
                        "must have exactly 1 parameter but has " + parameterTypes.length);
            }
        } else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
            String methodName = method.getDeclaringClass().getName() + "." + method.getName();
            throw new EventBusException(methodName +
                    " is a illegal @Subscribe method: must be public, non-static, and non-abstract");
        }
    }
}

這里走完,我們訂閱類的所有SubscriberMethod都已經被保存了,最后再通過getMethodsAndRelease()返回List<SubscriberMethod>至此,所有關于如何獲得訂閱類的訂閱方法信息即:SubscriberMethod對象就已經完全分析完了,下面我們來看subscribe()是如何實現的.

subscribe()方法的實現

這里我們回到subscribe(subscriber, subscriberMethod);中去,通過這個方法,我們就完成了注冊,下面看一下subscribe()的實現:

//必須在同步代碼塊里調用
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
    //獲取訂閱的事件類型
    Class<?> eventType = subscriberMethod.eventType;
    //創建Subscription對象
    Subscription newSubscription = new Subscription(subscriber, subscriberMethod);
    //從subscriptionsByEventType里檢查是否已經添加過該Subscription,如果添加過就拋出異常
    CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
    if (subscriptions == null) {
        subscriptions = new CopyOnWriteArrayList<>();
        subscriptionsByEventType.put(eventType, subscriptions);
    } else {
        if (subscriptions.contains(newSubscription)) {
            throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event "
                    + eventType);
        }
    }
    //根據優先級priority來添加Subscription對象
    int size = subscriptions.size();
    for (int i = 0; i <= size; i++) {
        if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {
            subscriptions.add(i, newSubscription);
            break;
        }
    }
    //將訂閱者對象以及訂閱的事件保存到typesBySubscriber里.
    List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
    if (subscribedEvents == null) {
        subscribedEvents = new ArrayList<>();
        typesBySubscriber.put(subscriber, subscribedEvents);
    }
    subscribedEvents.add(eventType);
    //如果接收sticky事件,立即分發sticky事件
    if (subscriberMethod.sticky) {
        //eventInheritance 表示是否分發訂閱了響應事件類父類事件的方法
        if (eventInheritance) {
            // Existing sticky events of all subclasses of eventType have to be considered.
            // Note: Iterating over all events may be inefficient with lots of sticky events,
            // thus data structure should be changed to allow a more efficient lookup
            // (e.g. an additional map storing sub classes of super classes: Class -> List<Class>).
            Set<Map.Entry<Class<?>, Object>> entries = stickyEvents.entrySet();
            for (Map.Entry<Class<?>, Object> entry : entries) {
                Class<?> candidateEventType = entry.getKey();
                if (eventType.isAssignableFrom(candidateEventType)) {
                    Object stickyEvent = entry.getValue();
                    checkPostStickyEventToSubscription(newSubscription, stickyEvent);
                }
            }
        } else {
            Object stickyEvent = stickyEvents.get(eventType);
            checkPostStickyEventToSubscription(newSubscription, stickyEvent);
        }
    }
}

以上就是所有注冊過程,現在再來看這張圖就會特別清晰EventBus的register()過程了:

image.png

2.事件分發(post)

我們知道可以通過EventBus.getDefault().post("str");來發送一個事件,所以我們就從這行代碼開始分析,首先看看post()方法是如何實現的:

public void post(Object event) {
    //得到當前線程的Posting狀態.
    PostingThreadState postingState = currentPostingThreadState.get();
    //獲取當前線程的事件隊列
    List<Object> eventQueue = postingState.eventQueue;
    eventQueue.add(event);

    if (!postingState.isPosting) {
        postingState.isMainThread = Looper.getMainLooper() == Looper.myLooper();
        postingState.isPosting = true;
        if (postingState.canceled) {
            throw new EventBusException("Internal error. Abort state was not reset");
        }
        try {
            //一直發送
            while (!eventQueue.isEmpty()) {
                //發送單個事件
                postSingleEvent(eventQueue.remove(0), postingState);
            }
        } finally {
            postingState.isPosting = false;
            postingState.isMainThread = false;
        }
    }
}

首先是通過currentPostingThreadState.get()方法來得到當前線程PostingThreadState的對象,為什么是說當前線程我們來看看currentPostingThreadState的實現:

private final ThreadLocal<PostingThreadState> currentPostingThreadState = new ThreadLocal<PostingThreadState>() {
    @Override
    protected PostingThreadState initialValue() {
        return new PostingThreadState();
    }
};

currentPostingThreadState的實現是一個包含了PostingThreadStateThreadLocal對象,關于ThreadLocal張濤的這篇文章解釋的很好:ThreadLocal 是一個線程內部的數據存儲類,通過它可以在指定的線程中存儲數據,而這段數據是不會與其他線程共享的。其內部原理是通過生成一個它包裹的泛型對象的數組,在不同的線程會有不同的數組索引值,通過這樣就可以做到每個線程通過get() 方法獲取的時候,取到的只能是自己線程所對應的數據。 所以這里取到的就是每個線程的PostingThreadState狀態.接下來我們來看postSingleEvent()方法:

private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
    Class<?> eventClass = event.getClass();
    boolean subscriptionFound = false;
    //是否觸發訂閱了該事件(eventClass)的父類,以及接口的類的響應方法.
    if (eventInheritance) {
        //查找eventClass類所有的父類以及接口
        List<Class<?>> eventTypes = lookupAllEventTypes(eventClass);
        int countTypes = eventTypes.size();
        //循環postSingleEventForEventType
        for (int h = 0; h < countTypes; h++) {
            Class<?> clazz = eventTypes.get(h);
            //只要右邊有一個為true,subscriptionFound就為true
            subscriptionFound |= postSingleEventForEventType(event, postingState, clazz);
        }
    } else {
        //post單個
        subscriptionFound = postSingleEventForEventType(event, postingState, eventClass);
    }
    //如果沒發現
    if (!subscriptionFound) {
        if (logNoSubscriberMessages) {
            Log.d(TAG, "No subscribers registered for event " + eventClass);
        }
        if (sendNoSubscriberEvent && eventClass != NoSubscriberEvent.class &&
                eventClass != SubscriberExceptionEvent.class) {
            //發送一個NoSubscriberEvent事件,如果我們需要處理這種狀態,接收這個事件就可以了
            post(new NoSubscriberEvent(this, event));
        }
    }
}

跟著上面的代碼的注釋,我們可以很清楚的發現是在postSingleEventForEventType()方法里去進行事件的分發,代碼如下:

private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {
    CopyOnWriteArrayList<Subscription> subscriptions;
    //獲取訂閱了這個事件的Subscription列表.
    synchronized (this) {
        subscriptions = subscriptionsByEventType.get(eventClass);
    }
    if (subscriptions != null && !subscriptions.isEmpty()) {
        for (Subscription subscription : subscriptions) {
            postingState.event = event;
            postingState.subscription = subscription;
            //是否被中斷
            boolean aborted = false;
            try {
                //分發給訂閱者
                postToSubscription(subscription, event, postingState.isMainThread);
                aborted = postingState.canceled;
            } finally {
                postingState.event = null;
                postingState.subscription = null;
                postingState.canceled = false;
            }
            if (aborted) {
                break;
            }
        }
        return true;
    }
    return false;
}

private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
    switch (subscription.subscriberMethod.threadMode) {
        case POSTING:
            invokeSubscriber(subscription, event);
            break;
        case MAIN:
            if (isMainThread) {
                invokeSubscriber(subscription, event);
            } else {
                mainThreadPoster.enqueue(subscription, event);
            }
            break;
        case BACKGROUND:
            if (isMainThread) {
                backgroundPoster.enqueue(subscription, event);
            } else {
                invokeSubscriber(subscription, event);
            }
            break;
        case ASYNC:
            asyncPoster.enqueue(subscription, event);
            break;
        default:
            throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
    }
}

總結上面的代碼就是,首先從subscriptionsByEventType里獲得所有訂閱了這個事件的Subscription列表,然后在通過postToSubscription()方法來分發事件,在postToSubscription()通過不同的threadMode在不同的線程里invoke()訂閱者的方法,ThreadMode共有四類:

  1. PostThread
    默認的 ThreadMode,表示在執行 Post 操作的線程直接調用訂閱者的事件響應方法,不論該線程是否為主線程(UI 線程)。當該線程為主線程時,響應方法中不能有耗時操作,否則有卡主線程的風險。適用場景:對于是否在主線程執行無要求,但若 Post 線程為主線程,不能耗時的操作;
  2. MainThread
    在主線程中執行響應方法。如果發布線程就是主線程,則直接調用訂閱者的事件響應方法,否則通過主線程的 Handler 發送消息在主線程中處理——調用訂閱者的事件響應函數。顯然,MainThread類的方法也不能有耗時操作,以避免卡主線程。適用場景:必須在主線程執行的操作
  3. BackgroundThread
    在后臺線程中執行響應方法。如果發布線程不是主線程,則直接調用訂閱者的事件響應函數,否則啟動唯一的后臺線程去處理。由于后臺線程是唯一的,當事件超過一個的時候,它們會被放在隊列中依次執行,因此該類響應方法雖然沒有PostThread類和MainThread類方法對性能敏感,但最好不要有重度耗時的操作或太頻繁的輕度耗時操作,以造成其他操作等待。適用場景:操作輕微耗時且不會過于頻繁,即一般的耗時操作都可以放在這里;
  4. Async
    不論發布線程是否為主線程,都使用一個空閑線程來處理。和BackgroundThread
    不同的是,Async類的所有線程是相互獨立的,因此不會出現卡線程的問題。適用場景:長耗時操作,例如網絡訪問。

這里我們只來看看invokeSubscriber(subscription, event);是如何實現的,關于不同線程的Poster的使用

void invokeSubscriber(Subscription subscription, Object event) {
    try {
        subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
    } catch (InvocationTargetException e) {
        handleSubscriberException(subscription, event, e.getCause());
    } catch (IllegalAccessException e) {
        throw new IllegalStateException("Unexpected exception", e);
    }
}

實際上就是通過反射調用了訂閱者的訂閱函數并把event對象作為參數傳入.至此post()流程就結束了,整體流程圖如下:

image.png

3.取消訂閱(unregister)

看完了上面的分析,解除注冊就相對容易了,解除注冊只要調用unregister()方法即可,實現如下:

public synchronized void unregister(Object subscriber) {
    //通過typesBySubscriber來取出這個subscriber訂閱者訂閱的事件類型,
    List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);
    if (subscribedTypes != null) {
        //分別解除每個訂閱了的事件類型
        for (Class<?> eventType : subscribedTypes) {
            unsubscribeByEventType(subscriber, eventType);
        }
        //從typesBySubscriber移除subscriber
        typesBySubscriber.remove(subscriber);
    } else {
        Log.w(TAG, "Subscriber to unregister was not registered before: " + subscriber.getClass());
    }
}

然后接著看unsubscribeByEventType()方法的實現:

private void unsubscribeByEventType(Object subscriber, Class<?> eventType) {
    //subscriptionsByEventType里拿出這個事件類型的訂閱者列表.
    List<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
    if (subscriptions != null) {
        int size = subscriptions.size();
        //取消訂閱
        for (int i = 0; i < size; i++) {
            Subscription subscription = subscriptions.get(i);
            if (subscription.subscriber == subscriber) {
                subscription.active = false;
                subscriptions.remove(i);
                i--;
                size--;
            }
        }
    }
}

最終分別從typesBySubscribersubscriptions里分別移除訂閱者以及相關信息即可.
觀察者模式觀察者模式是對象的行為模式,又叫發布-訂閱(Publish/Subscribe)模式、模型-視圖(Model/View)模式、源-監聽器(Source/Listener)模式或從屬者(Dependents)模式。觀察者模式定義了一種一對多的依賴關系,讓多個觀察者對象同時監聽某一個主題對象。這個主題對象在狀態上發生變化時,會通知所有觀察者對象,使它們能夠自動更新自己。EventBus并不是標準的觀察者模式的實現,但是它的整體就是一個發布/訂閱框架,也擁有觀察者模式的優點,比如:發布者和訂閱者的解耦.

4.拓展-注解

注解(Annotations)是一種元數據的格式,為程序提供數據,但又不是程序的一部分。注解(Annotations)在代碼上不直接影響代碼操作。這怎么理解是數據又不是程序的一部分?我們可以將注解看成一種標注,類似于注釋// 或 /* */,不是程序的一部分但為程序提供注釋,注解可以看作是提供數據。下面我們來看一下注解的用途:

  • 提供編譯信息:注解可以在編譯的時候檢查錯誤和提示警告(java語言規范性看來和注解不無關系)
  • 編譯和部署時處理:軟件工具可以通過注解生成代碼、xml文件等(注入看來也很黑科技)
  • 運行時處理:一些注解可以在運行時使用

注解基礎
注解是什么格式呢?

@Entity

這就是注解,看起來很簡單,通過設置(@)符號標識,在跟一個注解的名稱。下面來看一下我們經常用的Override注解。

@Override
void mySuperMethod() { ... }

再來看一下帶有參數的注解:

@Author(
   name = "Benjamin Franklin",
   date = "3/27/2003"
)
class MyClass() { ... }

or

@SuppressWarnings(value = "unchecked")
void myMethod() { ... }

如果按照上述value = "unchecked"是不是參數只能限定一個,如果多個怎么辦:

@SuppressWarnings("unchecked")
void myMethod() { ... }

其實還有一種能夠傳入多個值(Java SE 8 release支持),:

@Author(name = "Jane Doe")
@Author(name = "John Smith")
class MyClass { ... }

注解可以用在什么地方呢?注解可以用應用在:聲明一個類、變量、方法和程序元素。

聲明注解類型

如何做一個我們自己的注解?這里來看一個常見類:

public class Generation3List extends Generation2List {

   // Author: John Doe
   // Date: 3/17/2002
   // Current revision: 6
   // Last modified: 4/12/2004
   // By: Jane Doe
   // Reviewers: Alice, Bill, Cindy

   // class code goes here

}

這里使用相同的元數據,通過注解來實現,我們必須定義注解類型。語法如下:

@interface ClassPreamble {
   String author();
   String date();
   int currentRevision() default 1;
   String lastModified() default "N/A";
   String lastModifiedBy() default "N/A";
   // Note use of array
   String[] reviewers();
}

注解的定義非常類似接口,只是將接口的interface關鍵字替換為@interface。注解也是接口的一種形式。如何使用注解呢?注解的使用在聲明定義前使用:

@ClassPreamble (
   author = "John Doe",
   date = "3/17/2002",
   currentRevision = 6,
   lastModified = "4/12/2004",
   lastModifiedBy = "Jane Doe",
   // Note array notation
   reviewers = {"Alice", "Bob", "Cindy"}
)
public class Generation3List extends Generation2List {

// class code goes here

}

常用的注解類型
注解實際上被meta-annotations調用。幾種meta-annotationsjava.lang.annotation.類中。

  • @Retention 定義一種注解的存儲方式
  • -RetentionPolicy.SOURCE 注解以源碼級別存儲,忽略編譯
  • -RetentionPolicy.CLASS 注解在編譯時保留,忽略Java Virtual Machine (JVM)
  • -RetentionPolicy.RUNTIME 運行時注解
  • @Documented標識注解可以使用Javadoc tool工具將其生成文檔
  • @Target標識注解將要被應用成哪種Java元素,其中包含有:
  • -ElementType.ANNOTATION_TYPE
  • -ElementType.CONSTRUCTOR
  • -ElementType.FIELD
  • -ElementType.LOCAL_VARIABLE
  • -ElementType.METHOD
  • -ElementType.PACKAGE
  • -ElementType.PARAMETER
  • -ElementType.TYPE 任意一種類形式
  • @Inherited標識注解是否可以被子類集成(true或false,默認false),當用戶查詢注解類型,該類沒有聲明注解類型,該類的父類查詢到該注解類型。該注解僅應用在當前聲明類
  • @RepeatableJava SE 8引入,標識注解可以用應用多個相同的聲明,具體參考:Repeating Annotations.

EventBus對注解的使用
先來看一下注解類聲明:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Subscribe {
    ThreadMode threadMode() default ThreadMode.POSTING;

    /**
     * If true, delivers the most recent sticky event (posted with
     * {@link EventBus#postSticky(Object)}) to this subscriber (if event available).
     */
    boolean sticky() default false;

    /** Subscriber priority to influence the order of event delivery.
     * Within the same delivery thread ({@link ThreadMode}), higher priority subscribers will receive events before
     * others with a lower priority. The default priority is 0. Note: the priority does *NOT* affect the order of
     * delivery among subscribers with different {@link ThreadMode}s! */
    int priority() default 0;
}

注解支持文檔輸出,存在于運行時,對Java方法生效。在使用的時候如下代碼:

Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
if (subscribeAnnotation != null) {
    Class<?> eventType = parameterTypes[0];
    if (findState.checkAdd(method, eventType)) {
        ThreadMode threadMode = subscribeAnnotation.threadMode();
        findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,
        subscribeAnnotation.priority(), subscribeAnnotation.sticky()));
    }
}

獲取方法聲明的注解,通過subscribeAnnotation.threadMode()拿到對應的ThreadMode,通過subscribeAnnotation.priority()拿到聲明的優先級,通過subscribeAnnotation.sticky()拿到是否是粘性廣播。在方法前的聲明如下,聲明非常簡單:

@Subscribe(threadMode = ThreadMode.MAIN)

5.渦輪引擎-apt注入

apt注入可以重是一種編譯時注解,如何在gradle中進行動態注入呢?android-apt是一個Gradle插件,協助Android Studio處理annotation processors, 它有兩個目的:

  1. 允許配置只在編譯時作為注解處理器的依賴,而不添加到最后的APK或library
  2. 設置源路徑,使注解處理器生成的代碼能被Android Studio正確的引用

在EventBus上,主要使用注解處理器生成的代碼,將所有注解的內容獨立到org.greenrobot.eventbusperf.MyEventBusIndex類中,

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
        Messager messager = processingEnv.getMessager();
        try {
            // 獲取腳本中聲明變量eventBusIndex
            String index = processingEnv.getOptions().get(OPTION_EVENT_BUS_INDEX);
            ....
            // 收集訂閱的聲明
            collectSubscribers(annotations, env, messager);
           // 檢查訂閱聲明
            checkForSubscribersToSkip(messager, indexPackage);

            if (!methodsByClass.isEmpty()) {
                // 將訂閱聲明寫入到索引文件中
                createInfoIndexFile(index);
            } else {
                messager.printMessage(Diagnostic.Kind.WARNING, "No @Subscribe annotations found");
            }
            writerRoundDone = true;
        } catch (RuntimeException e) {
            // IntelliJ does not handle exceptions nicely, so log and print a message
            e.printStackTrace();
            messager.printMessage(Diagnostic.Kind.ERROR, "Unexpected error in EventBusAnnotationProcessor: " + e);
        }
        return true;
    }

關鍵就是將索引到的所有符合要求的注解類,寫入到索引文件中,代碼如下所示:

    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) {
            throw new RuntimeException("Could not write source for " + index, e);
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    //Silent
                }
            }
        }
    }

踩坑與經驗

1.混淆問題
混淆作為版本發布必備的流程,經常會鬧出很多奇奇怪怪的問題,且不方便定位,尤其是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住相關的方法和類。

2.跨進程問題
目前EventBus只支持跨線程,而不支持跨進程。如果一個app的service起到了另一個進程中,那么注冊監聽的模塊則會收不到另一個進程的EventBus發出的事件。這里可以考慮利用IPC做映射表,并在兩個進程中各維護一個EventBus,不過這樣就要自己去維護register和unregister的關系,比較繁瑣,而且這種情況下通常用廣播會更加方便,大家可以思考一下有沒有更優的解決方案。

3.事件環路問題
在使用EventBus時,通常我們會把兩個模塊相互監聽,來達到一個相互回調通信的目的。但這樣一旦出現死循環,而且如果沒有相應的日志信息,很難定位問題。所以在使用EventBus的模塊,如果在回調上有環路,而且回調方法復雜到了一定程度的話,就要考慮把接收事件專門封裝成一個子模塊,同時考慮避免出現事件環路。

老司機教你 “飆” EventBus 3
EventBus-3-0源碼分析
注解參考
android-apt 即將退出歷史舞臺
EventBus Documentation

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

推薦閱讀更多精彩內容