EventBus 3.0

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

Is what

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

流程图

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订阅相关的事件,并准备好处理事件的回调方法;
事件发布者则在适当的时机把事件post出去,EventBus就能搞定一切。

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

架构

核心类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

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


register

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

2.3 post

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

post

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

2.4 unregister

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


unregister
2.5 黏性事件

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

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

3 索引加速

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


速度对比

性能方面,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是不二之选。

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

推荐阅读更多精彩内容

  • 对于Android开发老司机来说肯定不会陌生,它是一个基于观察者模式的事件发布/订阅框架,开发者可以通过极少的代码...
    飞扬小米阅读 1,468评论 0 50
  • 我每周会写一篇源代码分析的文章,以后也可能会有其他主题.如果你喜欢我写的文章的话,欢迎关注我的新浪微博@达达达达s...
    SkyKai阅读 24,902评论 23 184
  • EventBus可以实现组件间通信,线程通信,比较方便灵活,2.x和3.0差别很多。 Android Studio...
    Sunyard_QiL阅读 788评论 0 2
  • 前言 在上一篇文章:EventBus 3.0初探: 入门使用及其使用 完全解析中,笔者为大家介绍了EventBus...
    丶蓝天白云梦阅读 15,807评论 21 128
  • 文|余语于隅 知道我和她的交往,很多人都不看好。 然而,我就是这样一个不信邪的人,万人要我往左,我非要往右,我总是...
    余语于隅阅读 316评论 4 7