EventBus 深入学习五之注册
订阅者的注册 + 消息推送
1. 注册
先贴出注册代码, 可以可到和 Guava 相比没什么大的区别, 主要的点在内部实现上,一个是如何获取注册信息;一个是如何保存注册关系
/** * Registers the given subscriber to receive events. Subscribers must call {@link #unregister(Object)} once they * are no longer interested in receiving events. * * Subscribers have event handling methods that must be annotated by {@link Subscribe}. * The {@link Subscribe} annotation also allows configuration like {@link * ThreadMode} and priority. */ public void register(Object subscriber) { Class subscriberClass = subscriber.getClass(); // 查询注册方法的核心 ListsubscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriberClass); synchronized (this) { for (SubscriberMethod subscriberMethod : subscriberMethods) { // 维护订阅关系的核心代码 subscribe(subscriber, subscriberMethod); } } }
获取订阅信息 (构建 SubscriberMethod
)
注解方式获取
从下面的代码可以看出,获取注解方法的流程是:
- 获取类的所有方法
- 只有一个参数的方法,判断是否有
@subscribe
注解 - 其他都过滤(视情况是否抛异常出来)
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"); } } }
对比Guava的获取注解方法, 实现的主要区别是Guava多了一个获取超类的过程
- Guava获取所有的超类, 根据每个类的 `getDeclaredMethods` 获取所有的方法,然后判断是否有注解- Greenrobot 则是直接调用类的 `getDeclaredMethods` 获取所有方法, 然后判断是否有注解
上面两个有什么区别 ?
class.getDeclaredMethods
可以获取类所有申明的方法,也就是说 private, protected, 默认,public四个作用域的都可以获取到,换句话说,Guava的订阅者方法可以是私有的!!!,即便父类的私有方法也是可以的, static也无所谓
Greenrobot
中限制了方法的作用域共有的非静态方法,有且只有一个参数,而且只是对当前类而言
非注解方式
支持非注解方式进行注册,主要借助SubscriberInfoIndex
来指定注册方法 。我们可以倒推一下这个设计思路:
- 注册,首先是要确定将类的哪些方法注册到
EventBus
- 排除掉注解方式;还有一种常见的就是我们定义一种方式,可以将我们需要注册的方法直接返回
- 定义一个接口,用于返回注册类的所有订阅信息,然后把这个接口也丢给
EventBus
下面贴一个非注解方式的测试用例,方便理解, SubscriberInfoIndex
就是我们定义用于返回所有的订阅关系的接口,通过调用 EventBus.addIndex
(非线程安全)方法告知eventbus
对象
public class EventBusIndexTest { private String value; @Test /** Ensures the index is actually used and no reflection fall-back kicks in. */ public void testManualIndexWithoutAnnotation() { SubscriberInfoIndex index = new SubscriberInfoIndex() { @Override public SubscriberInfo getSubscriberInfo(Class subscriberClass) { Assert.assertEquals(EventBusIndexTest.class, subscriberClass); SubscriberMethodInfo[] methodInfos = { new SubscriberMethodInfo("someMethodWithoutAnnotation", String.class) }; return new SimpleSubscriberInfo(EventBusIndexTest.class, false, methodInfos); } }; EventBus eventBus = EventBus.builder().addIndex(index).build(); eventBus.register(this); eventBus.post("Yepp"); eventBus.unregister(this); Assert.assertEquals("Yepp", value); } public void someMethodWithoutAnnotation(String value) { this.value = value; }}
下面则开始进入正式的代码纬度分析
private ListfindUsingInfo(Class subscriberClass) { FindState findState = prepareFindState(); findState.initForSubscriber(subscriberClass); while (findState.clazz != null) { 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); }
核心代码如下, 有意思的一点就是面对继承关系的处理,到底是选择子类的订阅关系还是父类的订阅关系
private SubscriberInfo getSubscriberInfo(FindState findState) { // 下面的判断逻辑主要针对子类继承了父类中的订阅方法时, 返回子类的订阅信息 if (findState.subscriberInfo != null && findState.subscriberInfo.getSuperSubscriberInfo() != null) { SubscriberInfo superclassInfo = findState.subscriberInfo.getSuperSubscriberInfo(); if (findState.clazz == superclassInfo.getSubscriberClass()) { return superclassInfo; } } if (subscriberInfoIndexes != null) { for (SubscriberInfoIndex index : subscriberInfoIndexes) { SubscriberInfo info = index.getSubscriberInfo(findState.clazz); if (info != null) { return info; } } } return null; }
维护注册关系
通过上面可以将订阅者所有注册方法找出来,找出来之后当然是要存起来,也就是这一小节的内容,如何存,存了之后如何用
private final Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;
这个东西保存的就是Event
->订阅方法
的映射关系, 相当于Guava的SubscriberRegistry
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) { // // 开始将订阅信息塞入 subscriptionsByEventType 时间-注册关系映射表中 Class eventType = subscriberMethod.eventType; Subscription newSubscription = new Subscription(subscriber, subscriberMethod); CopyOnWriteArrayListsubscriptions = 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); } } // 根据排序,从后往前判断,应该插入什么位置,也就是说相同的优先级,越早注册的,越先获取消息 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 > subscribedEvents = typesBySubscriber.get(subscriber); if (subscribedEvents == null) { subscribedEvents = new ArrayList<>(); 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 ). Set , Object>> entries = stickyEvents.entrySet(); for (Map.Entry , 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); } } }
##2. 取消注册
这个没什么好说的,就是上面的逆过程
3. 小结
上面分析了注册的流程,基本上思路和 Guava 的没什么大的区别,不同的是,做了更多的东西,实现了更多的功能,其中我们可以参考的几个设计思路
- 经典单例模式的
defaultInstance
, 这个在jdk里面用得非常多,比如Boolean.True
,Collections.EMPTY_LIST
,会给一些默认的实例,不用每次都去创建,上面的实现其实有不同的,实际对比之后比较明显可以感知 - 注解方式注册
- 如何获取某注解的方法,这个使用和guava的有些区别,特别是对超类以及作用域的情况处理
- 非注解方式注册
- 典型的借用辅助类(
SubscriberInfoIndex
)来完成预期目标
- 典型的借用辅助类(
此外实现的细节上也可以看看,参考优秀的写法才能提高自己的书写质量,当然每个人的习惯都不一样,就比如对EventBus类中的某些用法,本人持保留意见
大量使用内部类,且属性基本上都直接访问,不通过Getter/Setter方法 (讲道理,对这种方式还是不太习惯)EventBus中大量的配置属性,个人倾向使用Option配置类来做,会使类结构更加清晰