常用輪子之EventBus基本使用及原理

jasonhww發表於2019-01-20

不忘初心 砥礪前行, Tomorrow Is Another Day !

相關文章

本文概要:

  1. EventBus的介紹
  2. 簡單使用
  3. EventBus原始碼分析

引言

Android中訊息傳遞有多種方式.

  • Handler : 執行緒間的通訊.
  • BroadcastReceiver : 程式間的通訊.接收系統廣播.
  • 介面回撥 : 事件觸發通知.

當我們需要在多個地方接收事件通知,此時介面回撥過於繁瑣;廣播又顯得資源浪費.這時就需要用到EventBus了. EventBus是一種基於觀察者模式,降低元件之間耦合,簡化通訊的方式.
官方介紹地址: github.com/greenrobot/…

一. EventBus的介紹

EventBus的三劍客 :

  • Publisher : 事件釋出者
  • Event : 事件
  • Subscriber : 事件訂閱者

事件由釋出者通過EvenentBus傳遞給訂閱者.

image

EventBus的四模型 :

  • ThreadMode.POSTING : 在哪個執行緒釋出,就在哪個執行緒處理
  • ThreadMode.MAIN : 在主執行緒處理.
  • ThreadMode.BACKGROUND : 子~子 和 主~執行緒池
  • ThreadMode.ASYNC : 執行緒池處理

在3.0之前只能固定的方法來指定執行緒模型:

  • OnEvent
  • OnEventMainThread
  • OnEventBackgroundThread
  • OnEventAsync

二. 簡單使用

  1. 定義一個事件類
public class DataEvent{
    String data;
    public DataEvent(String data){
        this.data = data;
    }
}
複製程式碼
  1. 註冊訂閱
EventBus.getDefault().register(this);
複製程式碼
  1. 釋出事件
//釋出普通事件
EventBus.getDefault().post(new DataEvent());

//釋出黏性事件
EventBus.getDefault().postSticky(new DataEvent());
複製程式碼
  1. 定義訂閱方法
//指定執行緒模型和優先順序
@Subscriber (threadMode = ThreadMode.MAIN,priority = 0)
public void onDataEvent(DataEvent dataEvent){
}
複製程式碼
  1. 反註冊訂閱
EventBus.getDefault().unregister(this); 
複製程式碼

上述示例中,使用了黏性事件和優先順序. 接下來看兩者概念.

  • 黏性事件 : 事件傳送之後再進行註冊依然能收到該事件.
  • 優先順序 : 預設為0,數值越大優先順序越高.

三. 基本原理

EventBus的原理主要理解註冊-釋出事件-反註冊流程三個方面.

3.1 定義註解

同樣EventBus也定義了我們需要用到的註解.

@Documented
@Retention(RetentionPolicy.RUNTIME)//執行時註解
@Target({ElementType.METHOD})//修飾方法
public @interface Subscribe {
//定義了三個引數
    ThreadMode threadMode() default ThreadMode.POSTING;

    boolean sticky() default false;

    int priority() default 0;
}
複製程式碼

3.2 EvenBus的註冊

先初始化:使用單例模式雙重檢查DCL方式建立EventBus物件.

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

複製程式碼

初始化完成後,就需要呼叫register方法進行註冊.

public void register(Object subscriber) {
        Class<?> subscriberClass = subscriber.getClass();
        //獲取當前訂閱者所有訂閱方法(findSubscriberMethods)
        List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
        synchronized (this) {
            for (SubscriberMethod subscriberMethod : subscriberMethods) {
             //遍歷所有方法,將他們儲存起來(通過subscribe儲存).
                subscribe(subscriber, subscriberMethod);
            }
        }
    }
複製程式碼

接著看如何查到所有訂閱方法的

 List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
        List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass);
        //如果有快取,則直接返回
        if (subscriberMethods != null) {
            return subscriberMethods;
        }
        //無快取,則通過反射活編譯時生成的程式碼找到訂閱方法集合(具體實現稍後在分析,先了解到這就行)
        if (ignoreGeneratedIndex) {
            //反射獲取
            subscriberMethods = findUsingReflection(subscriberClass);
        } else {
            //3.0開啟索引加速
            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.put(subscriberClass, subscriberMethods);
            return subscriberMethods;
        }
    }
複製程式碼

當找到所有訂閱方法後,會儲存訂閱方法.我們看儲存的具體過程

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
        Class<?> eventType = subscriberMethod.eventType;
        Subscription newSubscription = new Subscription(subscriber, subscriberMethod);

        CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
        if (subscriptions == null) {
            subscriptions = new CopyOnWriteArrayList<>();
             //核心map,事件型別對應的Subscription訂閱資訊.
            //儲存在subscriptionsByEventType(Map,key:eventType(事件型別的class物件);
            // value:CopyOnWriteArrayList<Subscription> (Subscription集合))
           
            subscriptionsByEventType.put(eventType, subscriptions);
        } else {
            if (subscriptions.contains(newSubscription)) {
                throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event "
                        + eventType);
            }
        }

        int size = subscriptions.size();
        for (int i = 0; i <= size; i++) {
        //優先順序排序
            if (i == size || subscriberMethod.priority > subscriptions.get(i).subscriberMethod.priority) {
            //新增單個subscription
                subscriptions.add(i, newSubscription);
                break;
            }
        }

        List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
        if (subscribedEvents == null) {
            subscribedEvents = new ArrayList<>();

            //次核心map,訂閱者對應的所有事件型別
            // 儲存在typesBySubscriber(Map, key:subscriber(訂閱者); value:List<Class<?>>(事件型別的class物件集合))
            
            typesBySubscriber.put(subscriber, subscribedEvents);
        }
        subscribedEvents.add(eventType);

        if (subscriberMethod.sticky) {
            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);
            }
        }
    }
         
複製程式碼

3.3 EventBus的反註冊

註冊的時候核心是對核心Map和次核心Map,進行新增操作.那麼反註冊則是刪除操作.

public synchronized void unregister(Object subscriber) {
        //從次核心Map取出所有事件型別
        List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);
        if (subscribedTypes != null) {
            for (Class<?> eventType : subscribedTypes) {
                //從核心Map,移除訂閱資訊
                unsubscribeByEventType(subscriber, eventType);
            }
            //從次核心Map,移除訂閱者
            typesBySubscriber.remove(subscriber);
        } else {
            logger.log(Level.WARNING, "Subscriber to unregister was not registered before: " + subscriber.getClass());
        }
    }
    
    //從核心Map,移除訂閱資訊的具體過程
    private void unsubscribeByEventType(Object subscriber, Class<?> eventType) {
        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--;
                }
            }
        }
    }
複製程式碼

3.4 EventBus釋出事件

釋出事件,還是跟核心Map有很大關聯,接著看釋出事件流程.

由於Post方法內部呼叫了postSingleEvent來,其內部最終呼叫了postSingleEventForEventType傳送事件,這裡直接看該方法.

private boolean postSingleEventForEventType(Object event, PostingThreadState postingState, Class<?> eventClass) {
        CopyOnWriteArrayList<Subscription> subscriptions;
        synchronized (this) {
            //從核心Map,根據事件型別找到所有訂閱資訊
            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;
    }
    
  //postToSubscription的具體實現
  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 MAIN_ORDERED:
                if (mainThreadPoster != null) {
                    mainThreadPoster.enqueue(subscription, event);
                } else {
                    // temporary: technically not correct as poster not decoupled from subscriber
                    invokeSubscriber(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);
        }
    }   
複製程式碼

最後我們對整個EventBus流程做一個總結.

準備工作:
先初始化,使用單例模式雙重檢查DCL方式建立EventBus物件.

  1. 註冊:

    1. 獲取當前訂閱者所有訂閱方法(findSubscriberMethods)

      • 如果有快取(METHOD_CACHE),則直接返回; 無快取,則通過++反射或者編譯時生成的程式碼++來找到訂閱方法集合,然後快取起來返回.
    2. 遍歷所有方法,將他們儲存起來(通過subscribe儲存).

      1. 組合訂閱資訊,Subscription(訂閱資訊) = Subscriber(訂閱者) + SubscriberMethods(訂閱方法)
      2. 儲存在事件型別對應的訂閱資訊集合的Map中.(核心Map)
      3. ++儲存在訂閱者對應的所有事件型別(次核心Map)++
  2. 反註冊:

    1. 根據當前訂閱者,從次核心Map找到所有訂閱事件型別.
    2. 遍歷根據訂閱事件,從核心Map找到對應所有訂閱資訊.
    3. 最後一次移除訂閱資訊,訂閱者.
  3. 釋出事件

    • 根據當前事件型別,從核心Map找到所有訂閱資訊,遍歷呼叫.

3.4 EventBus的3.0索引加速

之前在講解EventBus的註冊過程時,只是簡單得講到獲取所有的訂閱方法,如果沒有快取時,則從反射或者編譯時註解生成的程式碼中獲取.接下來繼續看反射相關程式碼

a. 通過反射獲取所有訂閱方法
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;
        }
        //遍歷
        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();
                            //條件都滿足
                            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");
            }
        }
    }
複製程式碼
b. 通過編譯時生成的程式碼獲取所有訂閱方法
private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) {
        FindState findState = prepareFindState();
        findState.initForSubscriber(subscriberClass);
        while (findState.clazz != null) {
            //獲取訂閱資訊(getSubscriberInfo方法在編譯時索引類裡)
            findState.subscriberInfo = getSubscriberInfo(findState);
            if (findState.subscriberInfo != null) {
                //獲取所有訂閱方法
                SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods();
                for (SubscriberMethod subscriberMethod : array) {
                    if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) {
                        findState.subscriberMethods.add(subscriberMethod);
                    }
                }
            } else {
                //繼續回去呼叫反射
                findUsingReflectionInSingleClass(findState);
            }
            findState.moveToSuperclass();
        }
        return getMethodsAndRelease(findState);
    }

複製程式碼

到此為止,我們知道3.0之前通過反射獲取訂閱的方法,對於效能上是有所欠缺.所以在3.0後提供了(Subscribe Index)索引加速,其實本質就是註解處理器的應用,這樣就不用通過反射了,效能上得到了很大的提高.
接下來看如何開啟索引. greenrobot.org/eventbus/do…

1. 新增註解處理器:
android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                //指定生成的索引檔名以及日誌列印
                arguments = [ eventBusIndex : 'com.example.myapp.MyEventBusIndex', verbose : 'true' ]
            }
        }
    }
}
 
dependencies {
    implementation 'org.greenrobot:eventbus:3.1.1'
    annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.1.1'
}
複製程式碼

最後我們在MainActivity準備一些訂閱方法方法

    @Subscribe(threadMode = ThreadMode.MAIN, priority = 0)

    public void onDataEventOne(Integer dataEvent) {
    }

    @Subscribe(threadMode = ThreadMode.MAIN, priority = 1)
    public void onDataEventTwo(String dataEvent) {
    }

複製程式碼

接著編譯下專案,此時在build-generated-source-apt的包下,通過註解處理器生成了我們自己定義的索引檔案MyEventBusIndex.java

public class MyEventBusIndex implements SubscriberInfoIndex {
    private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;

    static {
        //索引Map,key為訂閱者,value為訂閱者資訊
        SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>();

        //新增訂閱者資訊到Map中
        putIndex(
                //
                new SimpleSubscriberInfo(org.jasonhww.eventbusdemo.MainActivity.class, true,
                new SubscriberMethodInfo[] {
            //建立訂閱方法物件
            new SubscriberMethodInfo("onDataEventOne", Integer.class, ThreadMode.MAIN),
            new SubscriberMethodInfo("onDataEventTwo", String.class, ThreadMode.MAIN, 1, false),
        }));

    }

    //將訂閱資訊新增到索引Map中
    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時新增我們配置好的索引.

//方法一,使用自定義的EventBus
EventBus eventBus = EventBus.builder().addIndex(new MyEventBusIndex()).build();
//方法二,使用預設的EventBus,大多數情況下采取這種就行.
EventBus.builder().addIndex(new MyEventBusIndex()).installDefaultEventBus();
EventBus eventBus = EventBus.getDefault();
複製程式碼
2. 註解處理器-建立索引類

在之前分析編譯時註解和ButterKnife,都有講到註解處理器的運用.同樣我們依葫蘆畫瓢來看EventBus是如何利用註解處理器去生成我們的索引類的.

相比之前分析的,這裡註解處理器類上多了一個@SupportedOptions(value = {"eventBusIndex", "verbose"})配置,這裡對應了最開始gradle的配置.

@SupportedAnnotationTypes("org.greenrobot.eventbus.Subscribe")
@SupportedOptions(value = {"eventBusIndex", "verbose"})
複製程式碼

這裡直接看註解處理器process方法裡,最重要的兩步.

  • 收集資訊

待生成的java檔案資訊儲存在methodsByClass中.

private void collectSubscribers(Set<? extends TypeElement> annotations, RoundEnvironment env, Messager messager) {
        for (TypeElement annotation : annotations) {//遍歷所有註解類對應的TypeElement
            //獲取被註解的元素(如MainActivity的onDataEventOne方法對應的元素物件)
            Set<? extends Element> elements = env.getElementsAnnotatedWith(annotation);
            for (Element element : elements) {
                if (element instanceof ExecutableElement) {
                    ExecutableElement method = (ExecutableElement) element;
                    if (checkHasNoErrors(method, messager)) {
                        //獲取被註解所在的類
                        TypeElement classElement = (TypeElement) method.getEnclosingElement();
                        //存入Map中,生成檔案時呼叫.  
                        methodsByClass.putElement(classElement, method);
                    }
                } else {
                    messager.printMessage(Diagnostic.Kind.ERROR, "@Subscribe is only valid for methods", element);
                }
            }
        }
    }
複製程式碼
  • 生成java檔案
    與ButterKnife不同,這裡使用了JDK的JavaFileObject建立索引類.可以對照之前生成的索引類.
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");
            //此裡面呼叫收集資訊時存入的Map(methodsByClass)
            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
                }
            }
        }
    }

複製程式碼

這樣就成功的將訂閱者的所有訂閱方法都儲存在了索引檔案了,這樣最終註冊的時候就不需要再利用反射區查詢訂閱方法了.

最後還是老規矩做一個小的總結.

所謂索引加速,就是利用註解處理器通過解析註解資訊,生成java檔案的索引類,將所有訂閱的方法儲存在索引類而已.

由於本人技術有限,如有錯誤的地方,麻煩大家給我提出來,本人不勝感激,大家一起學習進步.

參考連結:

相關文章