EventBus 3.0 黏性事件

干玲昌
前言:

在平时开发的过程中,经常会用到事件发布/订阅框架EventBus。知道基本用法,却一直没有深入去研究过源码。这样一知半解的情况下,踩了一个黏性事件的坑,终于也是“逮”到机会深入研究了下源码。但是本篇文章主要讲解黏性事件部分的源码,因为完整源码的解析,网上已经有足够优秀的文章了。

还是简单介绍下EventBus

EventBus是什么?

一套好奇三连甩出来,是什么?怎么用?为什么用?

是什么? EventBus是事件发布/订阅框架,主要用于Android中的组件和线程之间传递消息。

怎么用?

  • 注册订阅者
 EventBus.getDefault().register(this);
  • 响应订阅事件的方法
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
public void onMessageEvent(ObjectEvent event) {  
//这里做订阅到事件之后的处理
}
  • 发布事件
//发布普通事件
EventBus.getDefault().post(new ObjectEvent());  
//发布黏性事件
EventBus.getDefault().postSticky(new ObjectEvent());  
  • 注销订阅者
EventBus.getDefault().unregister(this);  

为什么用? 因为EventBus相对于其他的传递消息的方式(intent,handler...),使用简单,代码优雅,耦合性低。

基本使用方法介绍完了。这个时候可能会有人问?为什么要使用黏性事件,而不使用普通事件?因为点我达应用是保活的,所以在杀死手机应用的时候,轮询还是在执行的。如果在这个时候,轮询告诉你要弹出一个弹窗,你发布了一个普通的事件,但是这个时候应用只有一个Service服务在跑。并没有注册相应的事件,于是,普通事件就会丢失了。但是黏性事件就不一样了,黏性事件在事件发布之后再注册之后也能接收到。那么在源码上看是怎么实现的呢?

源码分析
public void postSticky(Object event) {  
        synchronized (stickyEvents) {
//private final Map<Class<?>, Object> stickyEvents;存储黏性事件
            stickyEvents.put(event.getClass(), event);
        }
        // Should be posted after it is putted, in case the subscriber wants to remove immediately
        post(event);
    }

如上代码,可以看到,与发布普通事件相比,在发布黏性事件的时候,多了一个步骤,将黏性事件存在一个stickyEvents的Map里面,以事件类名为键,事件对象为值存储起来。

public void register(Object subscriber) {  
        Class<?> subscriberClass = subscriber.getClass();
        List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass);
        synchronized (this) {
            //遍历全部订阅方法
            for (SubscriberMethod subscriberMethod : subscriberMethods) {
                //订阅操作
                subscribe(subscriber, subscriberMethod);
            }
        }
    }

public class SubscriberMethod {  
    final Method method;
    final ThreadMode threadMode;
    final Class<?> eventType;
    final int priority;
    final boolean sticky;
    /** Used for efficient comparison */
    String methodString;
}

如上代码,意思是在注册的时候,通过subscriberMethodFinder的findSubscriberMethods方法将订阅者的订阅信息找出来,存在一个列表中。然后遍历所有的订阅方法,执行订阅操作。SubscriberMethod这个类里面存储着订阅方法,线程类型,事件,是否是黏性事件,优先级等信息。下面来看subscribe()

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {  
        //这里省略号表示简短代码,去掉无关代码
        ...
       //是否是黏性事件
        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);
            }
        }
    }

如上代码,可以看到,如果订阅事件是黏性事件的话,这里有个stickyEvents,还记得这个东西吗?在上面发布黏性事件的时候,就已经存起来了。在注册的时候,会从缓存的黏性事件集合中取出,并且发布。哦!原来黏性消息在发布了事件之后再注册还能收到消息的奥秘是在这里,将发布的事件缓存起来,注册的时候再发布。接着来看下这个方法最后调用的方法

 private void checkPostStickyEventToSubscription(Subscription newSubscription, Object stickyEvent) {
        if (stickyEvent != null) {
            // If the subscriber is trying to abort the event, it will fail (event is not tracked in posting state)
            // --> Strange corner case, which we don't take care of here.
            postToSubscription(newSubscription, stickyEvent, Looper.getMainLooper() == Looper.myLooper());
        }
    }

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

如上代码,可以看到最后调用的postToSubscription这个方法,这里根据不同的ThreadMode,在不同的线程中调用订阅事件。具体是怎么调用的,这里用的是反射,具体细节不再详细展开。

以上代码分析,黏性事件在发布之后会将事件缓存下来,之后在注册的时候,会将黏性事件取出并发布给订阅者。但是普通的事件并不会这样,因此在发布普通事件的时候,如果这个时候没有注册订阅者,就会导致事件丢失了。 但是这样又会带来一个问题,如果没有及时的移除掉粘性事件的话,在某种“特殊”的情况下(特殊机型),会造成消息被多次消费。这就是上面提到的一个坑。解决这种问题的方法就是在收到黏性消息之后,马上移除掉黏性事件。

  public boolean removeStickyEvent(Object event) {
        synchronized (stickyEvents) {
            Class<?> eventType = event.getClass();
            Object existingEvent = stickyEvents.get(eventType);
            if (event.equals(existingEvent)) {
                stickyEvents.remove(eventType);
                return true;
            } else {
                return false;
            }
        }
    }

下面有两张图很好的概括了Register 和 post的流程。

总结

在平时的开发过程中,还是要你深入的了解所使用技术的实现原理,这样在开发过程中才能更得心应手的使用。否则的话,可能随时会踩一个坑。