手写一个简化版的EventBus

EventBus相信很多人都很熟悉,虽然现在谷歌官方出了JetPack来替代,但EventBus的一些设计思路还是值得借鉴的。下面就来写一个简单的EventBus案例

其实EventBus原理并不难,就是维护了几个数组,然后根据对应的key找到对应的注册对象,通过放射的方式调用对应的方法。

EventBus3.0之前和之后有比较大的区别,最大的差别在于3.0之后通过apt再编译期间生成一个引用对象,这样做很大程度上提高了性能。

最简单的使用
//注册事件
EventBus.getDefault().register(this);

//注册方法
@Subscribe
public void event(BaseEventBusBeaan message) {
  LogUtils.d("EventBusActivity event");
}

//发送事件
EventBus.getDefault().post(new BaseEventBusBeaan("123", new Bundle()));

//回收
EventBus.getDefault().unregister(this);

post流程

首先我们应该理清我们的需求,我们需要的是在 post一个对象出去的时候,所有注册监听了这个对象的类都能接收到这个通知,于是这里应该需要一个数组来存储数据。

//post出去的对象为key,一个注册者Subscription的list作为value
private Map<Class<?>, CopyOnWriteArrayList<Subscription>> subscriptionsByEventType;

//这个Subscription包括下面参数
public class Subscription {

    final Object subscriber;  //activity或者fragment
    final SubscriberMethod subscriberMethod;  

    public Subscription(Object subscriber, SubscriberMethod subscriberMethod) {
        this.subscriber = subscriber;
        this.subscriberMethod = subscriberMethod;
    }
}

public class SubscriberMethod {

    private String methodName; // 订阅方法名
    private Method method; // 订阅方法,用于最后的自动执行订阅方法
    private ThreadMode threadMode; // 线程模式
    private Class<?> eventType; // 事件对象Class,如:UserInfo.class
}

有了subscriptionsByEventType之后,我们就可以根据post()的发送的事件来查找所有注册者,再遍历list,逐一反射。

public void post(Object event) {
  postSingleEventForEventType(event, event.getClass());
}

//遍历所有的订阅者,发送对应的事件
private void postSingleEventForEventType(Object event, Class<?> eventClass) {
  CopyOnWriteArrayList<Subscription> subscriptions;

  synchronized (this) {
    subscriptions = subscriptionsByEventType.get(eventClass);
  }
  if (subscriptions != null && !subscriptions.isEmpty()) {
    for (Subscription subscription : subscriptions) {
        invokeSubscriber(subscription, event);
    }
  }
}

//这里暂时不考虑线程的问题
private void invokeSubscriber(Subscription subscription, Object event) {
  try {
    subscription.subscriberMethod.getMethod().invoke(subscription.subscriber, event);
  } catch (Exception e) {
    e.printStackTrace();
  }
}

上述就是一个简化版的post过程.

Register流程

上述的post还差一个很关键的地方,就是subscriptionsByEventType数据的来源,我们很自然的就该想到是在register的过程中。

再回来看看subscriptionsByEventType的key和value,发现这些值大都能从下面这样的函数中取得。

@Subscribe
public void event(BaseEventBusBeaan message) {
  LogUtils.d("EventBusActivity event");
}

所以我们需要遍历类中的所有方法,找到所有@Subscribe注释过的函数,并保存下来。

这里采用的是apt方案,在编译过程中遍历所有类,寻找所有@Subscribe注释过的函数,并将其按照一定格式保存下来,其结果会生成类似以下这样的类。

//具体的生成过程不再这里赘述,想要了解的可以自己看文末的代码
//编译过程中将所有 @Subscribe注释过的方法保存到SUBSCRIBER_INDEX数组中。
//key为函数所属的类,比如MainActivity,value则是一个对象,保存一个数据的集合。
public final class MyEventBusIndex implements SubscriberInfoIndex {
  private static final Map<Class, SubscriberInfo> SUBSCRIBER_INDEX;

  static {
    SUBSCRIBER_INDEX = new HashMap<Class,SubscriberInfo>();
    putIndex(new SimpleSubscriberInfo(EventBusActivity2.class,
            new SubscriberMethod[] {
                    new SubscriberMethod(EventBusActivity2.class, "event", BaseEventBusBeaan.class, ThreadMode.POSTING, 0, false),
                    new SubscriberMethod(EventBusActivity2.class, "sticky", UserInfo.class, ThreadMode.POSTING, 2, true),
                    new SubscriberMethod(EventBusActivity2.class, "sticky2", UserInfo.class, ThreadMode.POSTING, 2, true)}
                    ));
  }

  private static void putIndex(SubscriberInfo info) {
    SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info);
  }

  @Override
  public SubscriberInfo getSubscriberInfo(Class subscriberClass) {
    return SUBSCRIBER_INDEX.get(subscriberClass);
  }
}

有了MyEventBusIndex之后,开始register流程.

public void register(Object subscriber) {
  Class<?> subscriberClass = subscriber.getClass();
  List<SubscriberMethod> subscriberMethods = findSubscriberMethods(subscriberClass);
    
  //这个循环是生成subscriptionsByEventType对象的关键,
  for (SubscriberMethod method : subscriberMethods) {
    subscribe(subscriber, method);
  }
}

//1.根据subscriberClass先从methodBySubscriber缓存中找
private List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
  List<SubscriberMethod> subscriberMethods = methodBySubscriber.get(subscriberClass);
  if (subscriberMethods != null) return subscriberMethods;

  subscriberMethods = findByAPT(subscriberClass);
  if (subscriberMethods != null) {
    methodBySubscriber.put(subscriberClass, subscriberMethods);
  }

  return subscriberMethods;
}

//2.接着从subscriberInfoIndex查找,subscriberInfoIndex这个对象就是上文中提到的MyEventBusIndex的对象
private List<SubscriberMethod> findByAPT(Class<?> subscriberClass) {
  if (subscriberInfoIndex == null) {
    throw new RuntimeException("未添加索引文件");
  }
  SubscriberInfo subscriberInfo = subscriberInfoIndex.getSubscriberInfo(subscriberClass);
  if (subscriberInfo != null) return Arrays.asList(subscriberInfo.getSubscriberMethods());
  return null;
}

接着开始遍历subscriberMethods(因为每个订阅者不一定只有一个方法添加了@Subscribe注解)

for (SubscriberMethod method : subscriberMethods) {
  subscribe(subscriber, method);
}

//在这里就可以生成post过程中所需要的 subscriptionsByEventType 数据了。
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
  Class<?> eventType = subscriberMethod.getEventType();
  CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
  if (subscriptions == null) {
    subscriptions = new CopyOnWriteArrayList<>();
    subscriptionsByEventType.put(eventType, subscriptions);
  }

  Subscription subscription = new Subscription(subscriber, subscriberMethod);
  subscriptions.add(i, subscription);

  //订阅者类型集合,unregister的时候用到
  List<Class<?>> subscribeEvents = typeBySubscriber.get(subscriber);
  if (subscribeEvents == null) {
    subscribeEvents = new ArrayList<>();
    typeBySubscriber.put(subscriber, subscribeEvents);
  }
  subscribeEvents.add(eventType);
}

到了这里,其实一个简单的流程就已经通了。

总结一下大概的流程

  1. 通过apt在编译期将所有被 @Subscribe注解的函数添加到MyEventBusIndex对象中。
  2. register过程中生成subscriptionsByEventType的数据。
  3. post过程中通过subscriptionsByEventType数据查找对应的函数,然后再通过反射的方式调用。

优先级的问题

这个问题也十分简单,只需要在插入数据的时候,做下优先级判断即可。

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
  Class<?> eventType = subscriberMethod.getEventType();
  CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
  if (subscriptions == null) {
    subscriptions = new CopyOnWriteArrayList<>();
    subscriptionsByEventType.put(eventType, subscriptions);
  }

  Subscription subscription = new Subscription(subscriber, subscriberMethod);
  
  //根据优先级插队
  int size = subscriptions.size();
  for (int i = 0; i <= size; i++) {
    if (i == size || subscriberMethod.getPriority() > subscriptions.get(i).subscriberMethod.getPriority()) {
      if (!subscriptions.contains(subscription)) subscriptions.add(i, subscription);
      break;
    }
  }

  //订阅者类型集合,unregister的时候用到
  List<Class<?>> subscribeEvents = typeBySubscriber.get(subscriber);
  if (subscribeEvents == null) {
    subscribeEvents = new ArrayList<>();
    typeBySubscriber.put(subscriber, subscribeEvents);
  }
  subscribeEvents.add(eventType);
}

粘性事件

普通事件是先注册,后发送。而粘性事件相反,是先发送,后注册。

我们只需要调换一下顺序即可。在发送的时候将事件存储下来,然后在register的时候去检查有没有合适的事件

public void postSticky(Object event) {
  stickyEvents.put(event.getClass(), event);
}

private void subscribe(Object subscriber, SubscriberMethod subscriberMethod) {
        ....
    
    //检查是否有合适的事件可以触发
    sticky(subscriberMethod, eventType, subscription);
}

private void sticky(SubscriberMethod subscriberMethod, Class<?> eventType, Subscription subscription) {
  if (subscriberMethod.isSticky()) {
    Object event = stickyEvents.get(eventType);
    if (event != null) {
      postToSubscription(subscription, event);
    }
  }
}

最后加上postToSubscription的代码。

private void postToSubscription(final Subscription subscription, final Object event) {
  switch (subscription.subscriberMethod.getThreadMode()) {
    case POSTING: // 订阅、发布在同一线程
      invokeSubscriber(subscription, event);
      break;
    case MAIN:
      //事件发送方是主线程
      if (Looper.myLooper() == Looper.getMainLooper()) {
        invokeSubscriber(subscription, event);
      } else {
        //事件发送方是子线程
        handler.post(new Runnable() {
          @Override
          public void run() {
            invokeSubscriber(subscription, event);
          }
        });
      }
      break;
    case ASYNC:
      //发送方在主线程
      if (Looper.myLooper() == Looper.getMainLooper()) {
        executorService.execute(new Runnable() {
          @Override
          public void run() {
            invokeSubscriber(subscription, event);
          }
        });
      } else {
        invokeSubscriber(subscription, event);
      }
      break;
  }
}

private void invokeSubscriber(Subscription subscription, Object event) {
  try {
    subscription.subscriberMethod.getMethod().invoke(subscription.subscriber, event);
  } catch (Exception e) {
    e.printStackTrace();
  }
}

点击查看 源代码

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,265评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,274评论 1 288
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,087评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,479评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,782评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,218评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,594评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,316评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,955评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,274评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,803评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,177评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,732评论 3 229
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,953评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,687评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,263评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,189评论 2 258

推荐阅读更多精彩内容

  • 简介 我们知道,Android应用主要是由4大组件构成。当我们进行组件间通讯时,由于位于不同的组件,通信方式相对麻...
    Whyn阅读 499评论 0 1
  • 转:https://www.jianshu.com/p/d9516884dbd4[https://www.jian...
    enchanted1107阅读 386评论 0 0
  • EventBus用法及源码解析目录介绍1.EventBus简介1.1 EventBus的三要素1.2 EventB...
    杨充211阅读 1,836评论 0 4
  • EventBus是一个Android开源库,其使用发布/订阅模式,以提供代码间的松耦合。EventBus使用中央通...
    壮少Bryant阅读 636评论 0 4
  • EventBus是在Android中使用到的发布-订阅事件总线框架,基于观察者模式,将事件的发送者和接收者解耦,简...
    BrotherTree阅读 384评论 0 1