EventBus 总结

259 阅读8分钟

前言

本文章是在笔者在阅读 EventBus 源码后的一些总结,文章不涉及 EventBus 的流程源码分析。

问题

EventBus 的原理

EventBus-Publish-Subscribe.png

Eventbus 主要由4部分组成,分别是 发布者,订阅者,事件,事件总线。

  • 发布者:发布事件,调用 post() 发布普通事件,调用 postSticky() 发布黏性事件。
  • 订阅者:可以订阅自己需要的事件,调用 register() 后才能接收事件,不需要接收事件可以调用 unRegister() 。订阅者必须有订阅方法
  • 事件:事件可以分为普通事件可黏性事件。普通事件不会被事件总线保留,而黏性事件在被发布后就会被事件总线保留,直到手动移除。
  • 事件总线:负责接收和分发事件。

主要工作流程分为:订阅者注册,发布者发布事件,事件总线推送事件。

  • 订阅者注册

    注册主要有两步:1. 找出订阅者的所有订阅方法。2. 将每个订阅方法订阅者构成订阅关系,添加到事件总线中,为之后订阅者接收事件做准备。找订阅方法时,先从内存缓存中查找,内存缓存中没有时,默认优先使用索引类,当索引类不存在时才使用反射获取。把订阅关系添加到事件总线时,会检查订阅者是否订阅了黏性事件,如果有,就把黏性事件推送给订阅者。

  • 发布者发布事件

    发布普通事件用 post() ,发布黏性事件用 postSticky()postSticky() 只比 post() 多了一个步骤,就是先把黏性事件保存在事件总线中,再调用 post(),保存形式:Map<事件类型,事件实例>。具体的发布过程:通过 ThreadLocal 获得当前线程的事件队列,把要发布的事件入队,之后总线将事件逐个出队并推送给订阅者。

    private final Map<Class<?>, Object> stickyEvents;
    ​
    public void postSticky(Object event) {
        synchronized (stickyEvents) {
            stickyEvents.put(event.getClass(), event);
        }
        post(event);
    }
    
  • 总线推送事件

    post() 方法正常情况下会调用 postSingleEvent() ,再调用 postSingleEventForEventType() 。在postSingleEventForEventType() 方法中, 总线根据事件类型找出所有相关的订阅方法(订阅者,订阅方法),并调用 postToSubscription() 来推送事件。postToSubscription() 方法会根据订阅方法的线程模式作不同的处理,最后都会调用 invokeSubscriber() 来反射调用订阅方法。

五种线程模式

线程模式决定调用订阅方法的是哪个线程,对发布事件的线程没有影响。

  • POSTING:哪个线程发布就哪个线程调用订阅方法。
  • MAIN:如果发布事件的线程是主线程,则直接调用订阅方法;不是则通过 Handler 切换到主线程并且排队,等待被订阅者接收。
  • MAIN_ORDERED:不管发布线程的是不是主线程,都需要排队,等待被订阅者接收。
  • BACKGROUND:发布线程本身是子线程就直接调用订阅方法,不是则开启一个子线程调用。
  • ASYNC:开启新线程调用订阅方法。

线程切换实现原理

MAIN 和 MAIN_ORDERED 模式是通过 Handlelr 来切换线程的。代码主要在 HandlerPoster 这个类中(Android 平台中),这个类继承了 Handler ,重写了 handleMessage() ,Looper 是主线程的,内部有一个保存事件的队列,我们就叫这个类为发布器吧。当需要从子线程切换到主线程时,使用发布器 enqueue() ,将事件入队,如果发布器不是处于活跃状态的话,就发送一个空 Message ,在 Looper 里排队等待被主线程调用,从而完成从子线程到主线程的切换。

BACKGROUND 模式和 ASYNC 模式都是通过 线程池 来完成线程切换的。对应的类分别是 BackgroundPoster 和 AsyncPoster 。这两个类实现很相似,都实现了 Runnable 接口重写 run() ,内部和 HandlerPoster 一样有一个保存事件的队列。

MAIN 和 MAIN_ORDERED 的区别

MAIN_ORDERED 模式能保证订阅者接收事件的顺序事件的发布顺序是一致的,而 MAIN 模式不能。

举个通俗点的例子:

1个订阅方法,3个线程(主线程,子线程A,子线程B),这三个线程并发发布事件类型一样的事件,无论是 MAIN 模式还是 MAIN_ORDERED 模式,子线程发布的事件只能乖乖排队等待被接收。但是如果是在 MAIN 模式下,主线程发布的事件可以插队,MAIN_ORDERED 模式则不行。

//线程A发布事件5~9
new Thread(new Runnable() {
    @Override
    public void run() {
        for (int i=5;i<10;i++){
            EventBus.getDefault().post(String.valueOf(i));
        }
    }
}).start();
​
//线程B发布事件10~15
new Thread(new Runnable() {
    @Override
    public void run() {
        for (int i=10;i<15;i++){
            EventBus.getDefault().post(String.valueOf(i));
        }
    }
}).start();
​
//主线程发布事件0~5
for (int i=0;i<5;i++){
    EventBus.getDefault().post(String.valueOf(i));
}
​
//在EventBus中postToSubscription方法中添加打印语句,表示发布顺序
public void postToSubscription(...) {
    if (event instanceof String){
        System.out.println("FFF: 发布事件: "+event);
    }
    ...
}
​
//订阅方法的模式指定为 MAIN 
@Subscribe(threadMode = ThreadMode.MAIN)
    public void hello(String str) {
        System.out.println("JJJ 接收到事件 "+str);
    }
}

运行结果:

MAIN发布.PNG

MAIN接收.PNG

可以看到主线程发布的事件都插队了,而子线程发布的事件顺序是一致的。

再把线程模式修改为 MAIN_ORDERED ,运行结果如下:

MAIN_ORDERED发布.PNG

MAIN_ORDERED接收.PNG

可以看到事件的发布顺序和接收顺序是一致的。

MAIN , MAIN_ORDERED 模式下对防止主线程阻塞的优化

  • MAIN,MAIN_ORDERED 模式下,订阅方法本来就不应该执行耗时长的任务。一个订阅方法执行时间过长必定会阻塞主线程。

  • 但是如果主线程收到了大量事件,即使单个订阅方法的耗时不长,一次性接收完这些事件并执行订阅方法也是可能阻塞主线程的,EventBus 针对这种情况做了优化。

    HandlerPoster 继承 Handler, 重写了 handleMessage()

    @Override
    public void handleMessage(Message msg) {
        boolean rescheduled = false;
        try {
            // 获取一个开始时间
            long started = SystemClock.uptimeMillis();
            // 死循环
            while (true) {
                // 取出队列中最前面的元素
                PendingPost pendingPost = queue.poll();
                // 接下来会进行两次校验操作 
                // 判空 有可能队列中已经没有元素
                if (pendingPost == null) {
                    // 再次检查,这次是同步的
                    synchronized (this) {
                        // 继续取队头的元素
                        pendingPost = queue.poll();
                        if (pendingPost == null) {
                            // 还是为 null,将处理状态设置为不活跃,跳出循环
                            handlerActive = false;
                            return;
                        }
                    }
                }
                // 调用订阅者方法
                eventBus.invokeSubscriber(pendingPost);
                // 获得方法耗时
                long timeInMethod = SystemClock.uptimeMillis() - started;
                // 判断循环耗时是否超过预设值
                if (timeInMethod >= maxMillisInsideHandleMessage) {
                    if (!sendMessage(obtainMessage())) {
                        throw new EventBusException("Could not send handler message");
                    }
                    // 设置为活跃状态
                    rescheduled = true;
                    return;
                }
            }
        } finally {
            // 更新 Handle 状态
            handlerActive = rescheduled;
        }
    }
    

    在循环前记录一个开始时间,没调用完一次订阅者方法就更新耗时,这个耗时表示从循环开始到现在所调用的订阅者方法的总耗时,如果这个耗时超过预设值(默认为10ms),就发送一个空 Message ,结束方法,让其他"共享" Handler 执行任务,自己(HandlerPoster本身也是Handler)则继续排队。从而防止一次性推送多个事件而导致阻塞主线程。当然,如果单个订阅方法就耗时过长,这种优化也是没有用的,所以耗时长的订阅方法的线程模式不能是 MAIN,MAIN_ORDERD。

黏性事件的发布与接收

  • 黏性事件的发布比普通事件只多了一个步骤,就是把时间实例缓存在一个Map中,形式为:Map<事件类型,事件实例>。之后调用 post()
  • 黏性事件的接收分2种情况,第一种,事件发布前订阅者就存在,这种情况跟普通事件一样,是在post()方法调用后才调用 订阅方法。第二种,事件发布前订阅者不存在,调用订阅方法的时机是在 register() 中:先检查订阅方法是否接收黏性事件,在根据订阅方法的事件类型从总线中找出匹配的黏性事件,借着调用订阅方法。

EventBus 能否跨进程

不能,事件的发布与接收只能在同一个 EventBus 实例内,不同进程不能共享同一个实例

EventBus 的优缺点

  • 优点:

    • 简化不同组件之间的通讯
    • 解耦发送者和接收者
    • 快速,轻量,开销小
    • 可动态设置事件处理线程和优先级。
    • 避免复杂且容易出错的依赖关系和生命周期问题
  • 缺点:

    • 每个事件必须自定义一个事件类,增加了维护成本。

为什么只有 POSTING 线程模式能取消事件传递

private final ThreadLocal<PostingThreadState> currentPostingThreadState = new ThreadLocal<PostingThreadState>() {
    @Override
    protected PostingThreadState initialValue() {
        return new PostingThreadState();
    }
};
​
public void cancelEventDelivery(Object event) {
    PostingThreadState postingState = currentPostingThreadState.get();
    if (!postingState.isPosting) {
        throw new EventBusException(
            "This method may only be called from inside event handling methods on the posting thread");
    } else if (event == null) {
        throw new EventBusException("Event may not be null");
    } else if (postingState.event != event) {
        throw new EventBusException("Only the currently handled event may be aborted");
    } else if (postingState.subscription.subscriberMethod.threadMode != ThreadMode.POSTING) {
        throw new EventBusException(" event handlers may only abort the incoming event");
    }
    postingState.canceled = true;
}

可以看到,取消事件的传递是通过设置 postingState 的 canceled 来实现的。而 postingState 是通过 ThreadLocal 获得的,修改一个线程在 ThreadLocal 保存的变量显然对另外一个线程没有影响。而五种线程模式中,除了 POSTING 模式,其他模式都可能存在线程切换,所以取消事件传递只有 POSTING 模式支持。

与 Broadcast(广播) ,LocalBroadcast(本地广播) 的比较

  • 广播,四大组件之一,可用于进程间通信,也可以在进程内通信。使用 Broadcast 的开销太大,只在 APP 内部使用却要经过 system_server 进程,两次 Binder Call,并且存在被劫持的风险。

  • 本地广播的的设计模式与 EventBus 很像。使用 Intent 传递信息,使用 IntentFliter 过滤信息,需要用到一个 BroadcastReceiver接收信息,都需要注册和解注册,LocalBroadcastManager 相当于总线,负责接收,管理,发送信息。

  • EventBus 相比 LocalBroadcast 的优点:

    • LocalBroadcast 不够轻量,数据的传递依赖系统的 BroadcastReceiver ,里面糅合了很多跟我们业务无关的东西,违反了 迪米特原则。
    • LocalBroadcast 不支持指定接收线程,只有主线程能接收事件,通过 Handler 完成事件分发。
    • LocalBroadcast 不支持黏性事件,优先级,取消事件的传递。
    • EventBus 的使用起来更简洁。

EventBus 为什么不常被大项目使用

猜测:在多人协作开发时,如果发布事件不按照一个好的规范,可能导致乱发信息,而且较难找到信息的发送者,比如复用其他开发人员定义的事件。

eventInheritance(事件继承)

注册时对黏性事件的推送和普通事件的推送都会涉及到事件继承。如果事件之间没有继承关系,可以关闭事件继承(eventInheritance默认为true),可以提高性能。比如 lookupAllEventTypes() 可能会多次用到反射,这在事件之间没有继承关系时是没必要的。

APT

EventBus有两种方式寻找订阅方法,一种是利用反射,另外一种就是 APT 。EventBus 非常推荐用户使用 APT 来避免一些反射异常问题。使用方法在这:greenrobot.org/eventbus/do…。另外,使用反射查找订阅方法时,如果注册类的方法很多,必然会导致性能的下降,EventBus在这方面相关优化就是向上查找父类的方法时,如果父类是系统类,就会停止查找,否则像我们平常注册的 Activity 有大量的方法,每个都要需要遍历看看是不是订阅方法,这是完全没必要的。

参考博客

juejin.cn/post/684490…

juejin.cn/post/698995…

juejin.cn/post/710275…

blog.csdn.net/weixin_3024…