EventBus 3.0

作者:Markus Junginger
Github:greenrobot/EventBus
原文:老司机教你 “飙” EventBus 3

Is what

基于观察者模式的事件发布/订阅框架。
通过极少的代码实现模块间的通信,无须层层传递。使用方便,性能高,接入成本低,降低耦合,支持多线程。

EventBus 3.0_第1张图片
流程图

3.0 新特性

EventBus 3.0版本中引入了 EventBusAnnotationProcessor(注解分析生成索引)技术,大大提高了EventBus的运行效率。

1. 使用

流程图
1.1 添加依赖

build.gradle 中添加依赖:

compile'org.greenrobot:eventbus:3.0.0'
1.2 添加加速索引
  • 在项目gradledependencies中引入apt编译插件:
classpath'com.neenbedankt.gradle.plugins:android-apt:1.8'
  • Appbuild.gradle中应用apt插件,并设置apt生成的索引的包名和类名:
applyplugin:'com.neenbedankt.android-apt'
apt {
    arguments{
          eventBusIndex"com.study.sangerzhong.studyapp.MyEventBusIndex"
    }
}
  • Appdependencies中引入EventBusAnnotationProcessor
apt'org.greenrobot:eventbus-annotation-processor:3.0.1'

注意:

  1. 注解解析依赖于android-apt-plugin
  2. 加速索引可以不加;
  3. 应用EventBusAnnotationProcessor却没有设置arguments,编译时会报错:No option eventBusIndex passed to annotation processor,此时需要先编译一次,生成索引类。编译成功之后,会在\ProjectName\app\build\generated\source\apt\PakageName\下看到通过注解分析生成的索引类,如此便可在初始化EventBus时应用生成的索引了。
1.3 初始化

两种初始方式:

  1. 默认单例获取EventBus默认有一个实例)
EventBus mEventBus = EventBus.getDefault();
  1. 自定义
//如:应用生成好的索引时
EventBus mEventBus = EventBus.builder()
                .addIndex(new MyEventBusIndex())
                .build();
//如:自定义的设置应用到默认单例中
EventBus mEventBus = EventBus.builder()
                .addIndex(newMyEventBusIndex())
                .installDefaultEventBus();
1.4 定义事件

所有能被实例化为 Object 的实例都可以作为事件:

public class DriverEvent { 
    public String info; 
}

注意:若使用了索引加速,事件类的修饰符必须为public,否则编译时会报错:Subscriber method must be public

1.5 监听事件

在订阅事件(接收事件)的模块,注册EventBus

//如:Activity 中可写在 onCreate() 方法内
mEventBus.register(Object);

在订阅事件(接收事件)的模块,注销EventBus

//如:Activity 中可写在 onDestory() 方法内
mEventBus.unregister(Object);

3.0前,需要区分是否监听黏性(sticky)事件。
3.0中,改为添加注解的形式:

@Subscribe(threadMode = ThreadMode.POSTING, priority =0, sticky =true)
public void handleEvent ( DriverEventevent ){   
    Log.d(TAG,event.info);
}

注解有三个参数:

  • threadMode: 回调所在的线程
  • priority: 优先级
  • sticky: 是否接收黏性事件

注册监听模块必须有一个标注Subscribe注解方法,否则register时会抛出异常:
Subscriberclass XXX and its super classes havenopublic methods with the@Subscribeannotation

1.6 发送事件

调用postpostSticky即可。

接收事件的模块需要注册
发送事件的模块无须注册

EventBus.getDefault().post(new DriverEvent("magnet:?xt=urn:btih……"));

以上便完成了EventBus的学习。

总结

  • 实际使用中,registerunregister 通常与 ActivityFragment 的生命周期相关;
  • ThreadMode.MainThread解决了界面刷新必须在UI线程的问题;
  • 黏性事件可以解决了 postregister同时执行时的异步问题;
  • 事件传递没有序列化与反序列化的性能消耗。

2. 原理分析

2.1 和新架构
EventBus 3.0_第2张图片
机制

订阅者模块需要通过EventBus订阅相关的事件,并准备好处理事件的回调方法;
事件发布者则在适当的时机把事件post出去,EventBus就能搞定一切。

在架构方面,EventBus 3.0与之前稍老版本有不同,如图:

EventBus 3.0_第3张图片
架构

核心类EventBus,其中subscriptionByEventType是以事件的类为key,订阅者的回调方法为value的映射关系表。即EventBus在收到事件时,可根据该事件的类型,在subscriptionByEventType中找到所有监听了该事件的订阅者及处理事件的回调方法。typesBySubscriber是每个订阅者所监听的事件类型表,在取消注册时通过该表中保存的信息,快速删除subscriptionByEventType中订阅者的注册信息,避免遍历查找。注册事件、发送事件和注销都是围绕着这两个核心数据结构来展开。Subscription可以理解为每个订阅者与回调方法的关系,在其他模块发送事件时,会通过这个关系,让订阅者执行回调方法。

回调方法在这里被封装成了SubscriptionMethod,里面保存了在需要反射invoke方法时的各种参数,包括优先级,是否接收黏性事件和所在线程等信息。而要生成这些封装好的方法,则需要SubscriberMethodFinder,它可以在regster时得到订阅者的所有回调方法,并封装返回给EventBus。右边的加速器模块,是为了提高SubscriberMethodFinder的效率。

四种Poster:是EventBus能在不同的线程执行回调方法的核心

  • POSTING:调用post所在的线程执行回调,不需要poster调度,直接运行;
  • MAIN:UI线程回调,如果postUI线程则直接执行,否则通过mainThreadPoster调度;
  • BACKGROUND:Backgroud线程回调,如果post不在UI线程则直接执行,否则通过backgroundPoster调度;
  • ASYNC:交给线程池管理,直接通过asyncPoster调度。

不同的Poster会在post事件时,调度相应的事件队列PendingPostQueue,让每个订阅者的回调方法收到相应的事件,并在其注册的Thread中运行。而这个事件队列是一个链表,由一个个PendingPost组成,其中包含了事件,事件订阅者,回调方法这三个核心参数,以及需要执行的下一个PendingPost

2.2 register

根据订阅者的类来找回调方法,把订阅者和回调方法封装成关系,并保存到相应的数据结构中,为随后的事件分发做好准备,最后处理黏性事件:


EventBus 3.0_第4张图片
register

EventBus 3.0使用了注解表示回调,可以出现相同的ThreadMode的回调方法监听相同的事件,此时会根据注册的先后顺序,先注册先分发事件,注意不是先收到事件,收到事件的顺序还是得看posterHandler的调度。

2.3 post

分析事件后,得到所有监听该事件的订阅者的回调方法,并利用反射来invoke方法,实现回调:

EventBus 3.0_第5张图片
post

图中看到poster的调度事件功能,同时调度的单位细化成了Subscription,即每一个方法都有自己的优先级和是否接收黏性事件。源代码中为了保证post执行不会出现死锁,等待和对同一订阅者发送相同的事件,增加了很多线程保护锁和标志位。

2.4 unregister

把在注册时往两个数据结构中添加的订阅者信息删除即可:


EventBus 3.0_第6张图片
unregister
2.5 黏性事件

举栗:在登陆成功后自动播放歌曲,登陆和监听登陆是同时进行的。

  • 正常情况:如果登陆流程走得快,在登陆成功后播放模块才注册了监听。此时播放模块便错过了【登陆成功】的事件,出现“虽然登陆成功了,回调却没执行”的情况。
  • 粘性事件:如果【登陆成功】是黏性事件,即使后来才注册了监听(回调方法设置为监听黏性事件),则回调就能在注册的那一刻被执行,无需额外定义其他标志位。

3 索引加速

旧版本为了保证性能,在遍历寻找订阅者的回调方法时使用反射而不是注解。而新版本在使用注解的前提下,大幅度提高了性能。作者放出的对比图:


EventBus 3.0_第7张图片
速度对比

性能方面,EventBus 3.0由于使用了注解,比起使用反射来遍历方法的2.4版本逊色不少。但开启索引后性能远远超出旧版本。

关于索引加速的具体分析请看原文。

4 其他

4.1 混淆

EventBus 3.0使用注解的方式。作者的意思是在混淆时就不用再keep住相应的类和方法。

//运行时,抛出错误
java.lang.NoSuchFieldError: No static field POSTING。

//解决方法:keep住所有eventbus相关的代码
-keepclassde.greenrobot.** {*;}

分析,在SubscriberMethodFinder的findUsingReflection方法中,在调用Method.getAnnotation()时获取ThreadMode这个enum失败了,所以需要keep住这个enum(如下)。

-keeppublicenumorg.greenrobot.eventbus.ThreadMode {
    publicstatic*; 
}

这样就能编译通过了,如果使用了索引加速,是不会有上面这个问题的。因为在找方法时,调用的不是findUsingReflection,而是findUsingInfo

//使用索引加速后,编译后抛出错误:
Could not find subscriber method in XXX Class. Maybe a missing ProGuard rule?

因为生成索引GeneratedSubscriberIndex是在代码混淆之前进行的,混淆之后类名和方法名都不一样了(上面这个错误是方法无法找到),需要keep住所有被Subscribe注解标注的方法:

-keepclassmembersclass* {
    @de.greenrobot.event.Subscribe ;
}

这里就得权衡一下利弊:使用了注解不用索引加速,则只需要keepEventBus相关的代码,现有的代码可以正常的进行混淆。而使用了索引加速的话,则需要keep住相关的方法和类。

4.2 跨进程

目前EventBus支持跨线程,不支持跨进程。这里可以考虑利用IPC做映射表,并在两个进程中各维护一个EventBus,不过这样就要自己去维护registerunregister的关系,比较繁琐,这种情况下用广播更加方便。

4.3 事件环路

使用EventBus,通常会把两个模块相互监听,来达到相互回调通信的目的。一旦出现死循环,且没有相应的日志信息,很难定位问题。所以如果在回调上有环路,且回调方法十分复杂,就要考虑把接收事件专门封装成一个子模块,同时考虑避免出现事件环路。

5 写在最后

EventBus并不是重构代码的唯一之选。作为观察者模式的“同门师兄弟”——RxJava,作为功能更为强大的响应式编程框架,可以轻松实现EventBus的事件总线功能(RxBus)。但毕竟大型项目要接入RxJava的成本高,复杂的操作符需要开发者投入更多的时间去学习。所以在成熟的项目中快速地重构、解耦模块,EventBus是不二之选。

EventBus 3.0_第8张图片
老司机发车了

你可能感兴趣的:(EventBus 3.0)