Bugly 技术干货系列内容主要涉及移动开发方向,是由 Bugly 邀请腾讯内部各位技术大咖,经过平常工做经验的总结以及感悟撰写而成,内容均属原创,转载请标明出处。vue
EventBus对于Android开发老司机来讲确定不会陌生,它是一个基于观察者模式的事件发布/订阅框架,开发者能够经过极少的代码去实现多个模块之间的通讯,而不须要以层层传递接口的形式去单独构建通讯桥梁。从而下降因多重回调致使的模块间强耦合,同时避免产生大量内部类。它拥有使用方便,性能高,接入成本低和支持多线程的优势,实乃模块解耦、代码重构必备良药。java
做为Markus Junginger大神耗时4年打磨、超过1亿接入量、Github 9000+ star的明星级组件,分析EventBus的文章早已经是数不胜数。本文的题目是“教你飙巴士”,而这辆Bus之因此能够飙起来,是由于做者在EventBus 3中引入了EventBusAnnotationProcessor(注解分析生成索引)技术,大大提升了EventBus的运行效率。而分析这个加速器的资料在网上不多,所以本文会把重点放在分析这个EventBus 3的新特性上,同时分享一些踩坑经验,并结合源码分析及UML图,以直观的形式和你们一块儿学习EventBus 3的用法及运行原理。android
打开App的build.gradle,在dependencies中添加最新的EventBus依赖:c++
compile 'org.greenrobot:eventbus:3.0.0'
若是不须要索引加速的话,就能够直接跳到第二步了。而要应用最新的EventBusAnnotationProcessor则比较麻烦,由于注解解析依赖于android-apt-plugin。咱们一步一步来,首先在项目gradle的dependencies中引入apt编译插件:git
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
而后在App的build.gradle中应用apt插件,并设置apt生成的索引的包名和类名:github
apply plugin: 'com.neenbedankt.android-apt' apt { arguments { eventBusIndex "com.study.sangerzhong.studyapp.MyEventBusIndex" } }
接着在App的dependencies中引入EventBusAnnotationProcessor:编程
apt 'org.greenrobot:eventbus-annotation-processor:3.0.1'
这里须要注意,若是应用了EventBusAnnotationProcessor却没有设置arguments的话,编译时就会报错:No option eventBusIndex passed to annotation processor
数组
此时须要咱们先编译一次,生成索引类。编译成功以后,就会发如今\ProjectName\app\build\generated\source\apt\PakageName\
下看到经过注解分析生成的索引类,这样咱们即可以在初始化EventBus时应用咱们生成的索引了。缓存
EventBus默认有一个单例,能够经过getDefault()
获取,也能够经过EventBus.builder()
构造自定义的EventBus,好比要应用咱们生成好的索引时:微信
EventBus mEventBus = EventBus.builder().addIndex(new MyEventBusIndex()).build();
若是想把自定义的设置应用到EventBus默认的单例中,则能够用installDefaultEventBus()
方法:
EventBus.builder().addIndex(new MyEventBusIndex()).installDefaultEventBus();
全部能被实例化为Object的实例均可以做为事件:
public class DriverEvent { public String info; }
在最新版的eventbus 3中若是用到了索引加速,事件类的修饰符必须为public,否则编译时会报错:Subscriber method must be public
首先把做为订阅事件的模块经过EventBus注册监听:
mEventBus.register(this);
在3.0以前,注册监听须要区分是否监听黏性(sticky)事件,监听EventBus事件的模块须要实现以onEvent开头的方法。现在改成在方法上添加注解的形式:
@Subscribe(threadMode = ThreadMode.POSTING, priority = 0, sticky = true) public void handleEvent(DriverEvent event) { Log.d(TAG, event.info); }
注解有三个参数,threadMode为回调所在的线程,priority为优先级,sticky为是否接收黏性事件。调度单位从类细化到了方法,对方法的命名也没有了要求,方便混淆代码。但注册了监听的模块必须有一个标注了Subscribe注解方法,否则在register时会抛出异常:Subscriber class XXX and its super classes have no public methods with the @Subscribe annotation
调用post或者postSticky便可:
mEventBus.post(new DriverEvent("magnet:?xt=urn:btih……"));
到此咱们就完成了使用EventBus的学习,能够在代码中尽情地飚车了。项目接入了EventBus以后会有什么好处呢?举一个常见的用例:ViewPager中Fragment的相互通讯,就不须要在容器中定义各类接口,能够直接经过EventBus来实现相互回调,这样就把逻辑从ViewPager这个容器中剥离出来,使代码阅读起来更加直观。
在实际项目的使用中,register和unregister一般与Activity和Fragment的生命周期相关,ThreadMode.MainThread能够很好地解决Android的界面刷新必须在UI线程的问题,不须要再回调后用Handler中转(EventBus中已经自动用Handler作了处理),黏性事件能够很好地解决post与register同时执行时的异步问题(这个在原理中会说到),事件的传递也没有序列化与反序列化的性能消耗,足以知足咱们大部分状况下的模块间通讯需求。
在平时使用中咱们不须要关心EventBus中对事件的分发机制,但要成为可以快速排查问题的老司机,咱们仍是得熟悉它的工做原理,下面咱们就透过UML图来学习一下。
EventBus的核心工做机制透过做者Blog中的这张图就能很好地理解:
订阅者模块须要经过EventBus订阅相关的事件,并准备好处理事件的回调方法,而事件发布者则在适当的时机把事件post出去,EventBus就能帮咱们搞定一切。在架构方面,EventBus 3与以前稍老版本有不一样,咱们直接看架构图:
先看核心类EventBus,其中subscriptionByEventType是以事件的类为key,订阅者的回调方法为value的映射关系表。也就是说EventBus在收到一个事件时,就能够根据这个事件的类型,在subscriptionByEventType中找到全部监听了该事件的订阅者及处理事件的回调方法。而typesBySubscriber则是每一个订阅者所监听的事件类型表,在取消注册时能够经过该表中保存的信息,快速删除subscriptionByEventType中订阅者的注册信息,避免遍历查找。注册事件、发送事件和注销都是围绕着这两个核心数据结构来展开。上面的Subscription能够理解为每一个订阅者与回调方法的关系,在其余模块发送事件时,就会经过这个关系,让订阅者执行回调方法。
回调方法在这里被封装成了SubscriptionMethod,里面保存了在须要反射invoke方法时的各类参数,包括优先级,是否接收黏性事件和所在线程等信息。而要生成这些封装好的方法,则须要SubscriberMethodFinder,它能够在regster时获得订阅者的全部回调方法,并封装返回给EventBus。而右边的加速器模块,就是为了提升SubscriberMethodFinder的效率,会在第三章详细介绍,这里就再也不啰嗦。
而下面的三个Poster,则是EventBus能在不一样的线程执行回调方法的核心,咱们根据不一样的回调方式来看:
能够看到,不一样的Poster会在post事件时,调度相应的事件队列PendingPostQueue,让每一个订阅者的回调方法收到相应的事件,并在其注册的Thread中运行。而这个事件队列是一个链表,由一个个PendingPost组成,其中包含了事件,事件订阅者,回调方法这三个核心参数,以及须要执行的下一个PendingPost。
至此EventBus 3的架构就分析完了,与以前EventBus老版本最明显的区别在于:分发事件的调度单位从订阅者,细化成了订阅者的回调方法。也就是说每一个回调方法都有本身的优先级,执行线程和是否接收黏性事件,提升了事件分发的灵活程度,接下来咱们在看核心功能的实现时更能体现这一点。
简单来讲就是:根据订阅者的类来找回调方法,把订阅者和回调方法封装成关系,并保存到相应的数据结构中,为随后的事件分发作好准备,最后处理黏性事件:
值得注意的是,老版本的EventBus是容许事件订阅者以不一样的ThreadMode去监听同一个事件的,即在一个订阅者中有多个方法订阅一个事件,此时是没法保证这几个回调的前后顺序的,由于不一样的线程回调是经过Handler调度的,有可能单个线程中的事件过多,事件受阻,回调则会比较慢。现在EventBus 3使用了注解来表示回调后,还能够出现相同的ThreadMode的回调方法监听相同的事件,此时会根据注册的前后顺序,先注册先分发事件,注意不是先收到事件,收到事件的顺序仍是得看poster中Handler的调度。
总的来讲就是分析事件,获得全部监听该事件的订阅者的回调方法,并利用反射来invoke方法,实现回调:
这里就能看到poster的调度事件功能,同时能够看到调度的单位细化成了Subscription,即每个方法都有本身的优先级和是否接收黏性事件。在源代码中为了保证post执行不会出现死锁,等待和对同一订阅者发送相同的事件,增长了不少线程保护锁和标志位,值得咱们每一个开发者学习。
注销就比较简单了,把在注册时往两个数据结构中添加的订阅者信息删除便可:
上面常常会提到黏性事件,为何要有这个设计呢?这里举个例子:我想在登录成功后自动播放歌曲,而登录和监听登录监听是同时进行的。在这个前提下,若是登录流程走得特别快,在登录成功后播放模块才注册了监听。此时播放模块便会错过了【登录成功】的事件,出现“虽然登录成功了,回调却没执行”的状况。而若是【登录成功】这个事件是一个黏性事件的话,那么即便我后来才注册了监听(而且回调方法设置为监听黏性事件),则回调就能在注册的那一刻被执行,这个问题就能被优雅地解决,而不须要额外去定义其余标志位。
至此你们对EventBus的运行原理应该有了必定的了解,虽然看起来像是一个复杂耗时的自动机,但大部分时候事件都是一瞬间就能分发到位的,而你们关心的性能问题反而是发生在注册EventBus的时候,由于须要遍历监听者的全部方法去找到回调的方法。做者也提到运行时注解的性能在Android上并不理想,为了解决这个问题,做者才会以索引的方式去生成回调方法表(下一章会详细介绍)。而EventBus源码分析的文章早已经是数不胜数,这里就再也不大段大段地贴代码了,主要以类图和流程图的形式让你们直观地了解EventBus3的总体架构及核心功能的实现原理,把源码分析留到后面介绍EventBusAnnotationProcessor中再进行。你们若是想要深刻学习EventBus 3的话,在本文结尾的参考文章中有不少写得很棒的源码分析。
在EventBus 3的介绍中,做者提到之前的版本为了保证性能,在遍历寻找订阅者的回调方法时使用反射而不是注解。但如今却能在使用注解的前提下,大幅度提升性能,同时做者在博客中放出了这张对比图:
能够看到在性能方面,EventBus 3因为使用了注解,比起使用反射来遍历方法的2.4版本逊色很多。但开启了索引后性能像打了鸡血同样,远远超出以前的版本。这里咱们就来分析一下这个提升EventBus性能的“涡轮引擎”。(下面的源码分析为了方便阅读,添加了部分注释,并删减了部分源码,若是有疑问的话能够到官方的github上查看原版源码)
首先咱们知道,索引是在初始化EventBus时经过EventBusBuilder.addIndex(SubscriberInfoIndex index)
方法传进来的,咱们就先看看这个方法:
public EventBusBuilder addIndex(SubscriberInfoIndex index) { if(subscriberInfoIndexes == null) { subscriberInfoIndexes = new ArrayList<>(); } subscriberInfoIndexes.add(index); return this; }
能够看到,传进来的索引信息会保存在subscriberInfoIndexes这个List中,后续会经过EventBusBuilder传到相应EventBus的SubscriberMethodFinder实例中。咱们先来分析SubscriberInfoIndex这个参数:
public interface SubscriberInfoIndex { SubscriberInfo getSubscriberInfo(Class<?> subscriberClass); }
可见索引只须要作一件事情——就是能拿到订阅者的信息。而实现这个接口的类若是咱们没有编译过,是找不到的。这里就得看咱们在一开始在配置gradle时导入的EventBusAnnotationProcessor:
@SupportedAnnotationTypes("org.greenrobot.eventbus.Subscribe") @SupportedOptions(value = {"eventBusIndex", "verbose"}) public class EventBusAnnotationProcessor extends AbstractProcessor { /** Found subscriber methods for a class (without superclasses). 被注解表示的方法信息 */ private final ListMap<TypeElement, ExecutableElement> methodsByClass = new ListMap<>(); private final Set<TypeElement> classesToSkip = new HashSet<>(); // checkHasErrors检查出来的异常方法 @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) { Messager messager = processingEnv.getMessager(); try { String index = processingEnv.getOptions().get(OPTION_EVENT_BUS_INDEX); if (index == null) { // 若是没有在gradle中配置apt的argument,编译就会在这里报错 messager.printMessage(Diagnostic.Kind.ERROR, "No option " + OPTION_EVENT_BUS_INDEX + " passed to annotation processor"); return false; } /** ... */ collectSubscribers(annotations, env, messager); // 根据注解拿到全部订阅者的回调方法信息 checkForSubscribersToSkip(messager, indexPackage); // 筛掉不符合规则的订阅者 if (!methodsByClass.isEmpty()) { createInfoIndexFile(index); // 生成索引类 } /** 打印错误 */ } /** 下面这些方法就再也不贴出具体实现了,咱们了解它们的功能就行 */ private void collectSubscribers // 遍历annotations,找出全部被注解标识的方法,以初始化methodsByClass private boolean checkHasNoErrors // 过滤掉static,非public和参数大于1的方法 private void checkForSubscribersToSkip // 检查methodsByClass中的各个类,是否存在非public的父类和方法参数 /** 下面这三个方法会把methodsByClass中的信息写到相应的类中 */ private void writeCreateSubscriberMethods private void createInfoIndexFile private void writeIndexLines }
至此便揭开了索引生成的秘密,是在编译时apt插件经过EventBusAnnotationProcessor分析注解,并利用注解标识的相关类的信息去生成相关的类。writeCreateSubscriberMethods中调用了不少IO函数,很容易理解,这里就不贴了,咱们直接看生成出来的类:
/** This class is generated by EventBus, do not edit. */ public class MyEventBusIndex implements SubscriberInfoIndex { private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX; static { SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>(); // 每有一个订阅者类,就调用一次putIndex往索引中添加相关的信息 putIndex(new SimpleSubscriberInfo(com.study.sangerzhong.studyapp.ui.MainActivity.class, true, new SubscriberMethodInfo[] { new SubscriberMethodInfo("onEvent", com.study.sangerzhong.studyapp.ui.MainActivity.DriverEvent.class, ThreadMode.POSTING, 0, false), // 类中每个被Subscribe标识的方法都在这里添加进来 })); } // 下面的代码就是EventBusAnnotationProcessor中写死的了 private static void putIndex(SubscriberInfo info) { SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info); } @Override public SubscriberInfo getSubscriberInfo(Class<?> subscriberClass) { SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass); if (info != null) { return info; } else { return null; } } }
可见,子类中hardcode了全部注册了EventBus的类中被Subscribe注解标识的方法信息,包括方法名、方法参数类型等信息。并把这些信息封装到SimpleSubscriberInfo中,咱们拿到的索引其实就是以订阅者的类为Key、SimpleSubscriberInfo为value的哈希表。而这些hardcode都是在编译的时候生成的,避免了在在EventBus.register()时才去遍历查找生成,从而把在注册时须要遍历订阅者全部方法的行为,提早到在编译时完成了。
索引的生成咱们已经明白了,那么它是在哪里被应用的呢?咱们记得在注册时,EventBus会经过SubscriberMethodFinder来遍历注册对象的Class的全部方法,筛选出符合规则的方法,并做为订阅者在接收到事件时执行的回调,咱们直接来看看SubscriberMethodFinder.findSubscriberMethods()
这个核心方法:
List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) { List<SubscriberMethod> subscriberMethods = METHOD_CACHE.get(subscriberClass); if (subscriberMethods != null) { return subscriberMethods; // 先去方法缓存里找,找到直接返回 } if (ignoreGeneratedIndex) { // 是否忽略设置的索引 subscriberMethods = findUsingReflection(subscriberClass); } else { subscriberMethods = findUsingInfo(subscriberClass); } /** 把找到的方法保存到METHOD_CACHE里并返回,找不到直接抛出异常 */ }
能够看到其中findUsingInfo()方法就是去索引中查找订阅者的回调方法,咱们戳进去看看这个方法的实现:
private List<SubscriberMethod> findUsingInfo(Class<?> subscriberClass) { // 最新版的EventBus3中,寻找方法时所需的临时变量都被封装到了FindState这个静态内部类中 FindState findState = prepareFindState(); // 到对象池中取得上下文,避免频繁创造对象,这个设计很赞 findState.initForSubscriber(subscriberClass); // 初始化寻找方法的上下文 while (findState.clazz != null) { // 子类找完了,会继续去父类中找 findState.subscriberInfo = getSubscriberInfo(findState); // 得到订阅者类的相关信息 if (findState.subscriberInfo != null) { // 上一步能拿到相关信息的话,就开始把方法数组封装成List SubscriberMethod[] array = findState.subscriberInfo.getSubscriberMethods(); for (SubscriberMethod subscriberMethod : array) { if (findState.checkAdd(subscriberMethod.method, subscriberMethod.eventType)) { // checkAdd是为了不在父类中找到的方法是被子类重写的,此时应该保证回调时执行子类的方法 findState.subscriberMethods.add(subscriberMethod); } } } else { // 索引中找不到,降级成运行时经过注解和反射去找 findUsingReflectionInSingleClass(findState); } findState.moveToSuperclass(); // 上下文切换成父类 } return getMethodsAndRelease(findState); // 找完后,释放FindState进对象池,并返回找到的回调方法 }
能够看到EventBus中在查找订阅者的回调方法时是能处理好继承关系的,不只会去遍历父类,并且还会避免由于重写方法致使执行屡次回调。其中须要关心的是getSubscriberInfo()是如何返回索引数据的,咱们继续深刻:
private SubscriberInfo getSubscriberInfo(FindState findState) { if (findState.subscriberInfo != null && findState.subscriberInfo.getSuperSubscriberInfo() != null) { // subscriberInfo已有实例,证实本次查找须要查找上次找过的类的父类 SubscriberInfo superclassInfo = findState.subscriberInfo.getSuperSubscriberInfo(); if (findState.clazz == superclassInfo.getSubscriberClass()) { // 肯定是所需查找的类 return superclassInfo; } } if (subscriberInfoIndexes != null) { // 从咱们传进来的subscriberInfoIndexes中获取相应的订阅者信息 for (SubscriberInfoIndex index : subscriberInfoIndexes) { SubscriberInfo info = index.getSubscriberInfo(findState.clazz); if (info != null) { return info; } } } return null; }
可见就在这个方法里面,应用到了咱们生成的索引,避免咱们须要在findSubscriberMethods时去调用耗时的findUsingReflection方法。在实际使用中,Nexus6上一个Activity注册EventBus须要10毫秒左右,而使用了索引后能下降到3毫秒左右,效果很是明显。虽然这个索引的实现逻辑有点绕,并且还存在一些坑(好比后面讲到的混淆问题),但实现的手段很是巧妙,尤为是“把耗时的操做在编译的时候完成”和“用对象池减小建立对象的性能开销”的思想值得咱们开发者借鉴。
混淆做为版本发布必备的流程,常常会闹出不少奇奇怪怪的问题,且不方便定位,尤为是EventBus这种依赖反射技术的库。一般状况下都会把相关的类和回调方法都keep住,但这样其实会留下被人反编译后破解的后顾之忧,因此咱们的目标是keep最少的代码。
首先,由于EventBus 3弃用了反射的方式去寻找回调方法,改用注解的方式。做者的意思是在混淆时就不用再keep住相应的类和方法。可是咱们在运行时,却会报java.lang.NoSuchFieldError: No static field POSTING
。网上给出的解决办法是keep住全部eventbus相关的代码:
-keep class de.greenrobot.** {*;}
其实咱们仔细分析,能够看到是由于在SubscriberMethodFinder的findUsingReflection方法中,在调用Method.getAnnotation()时获取ThreadMode这个enum失败了,因此咱们只须要keep住这个enum就能够了(以下)。
-keep public enum org.greenrobot.eventbus.ThreadMode { public static *; }
这样就能正常编译经过了,但若是使用了索引加速,是不会有上面这个问题的。由于在找方法时,调用的不是findUsingReflection,而是findUsingInfo。可是使用了索引加速后,编译后却会报新的错误:Could not find subscriber method in XXX Class. Maybe a missing ProGuard rule?
这就很好理解了,由于生成索引GeneratedSubscriberIndex是在代码混淆以前进行的,混淆以后类名和方法名都不同了(上面这个错误是方法没法找到),得keep住全部被Subscribe注解标注的方法:
-keepclassmembers class * { @de.greenrobot.event.Subscribe <methods>; }
因此又倒退回了EventBus2.4时不能混淆onEvent开头的方法同样的处境了。因此这里就得权衡一下利弊:使用了注解不用索引加速,则只须要keep住EventBus相关的代码,现有的代码能够正常的进行混淆。而使用了索引加速的话,则须要keep住相关的方法和类。
目前EventBus只支持跨线程,而不支持跨进程。若是一个app的service起到了另外一个进程中,那么注册监听的模块则会收不到另外一个进程的EventBus发出的事件。这里能够考虑利用IPC作映射表,并在两个进程中各维护一个EventBus,不过这样就要本身去维护register和unregister的关系,比较繁琐,并且这种状况下一般用广播会更加方便,你们能够思考一下有没有更优的解决方案。
在使用EventBus时,一般咱们会把两个模块相互监听,来达到一个相互回调通讯的目的。但这样一旦出现死循环,并且若是没有相应的日志信息,很难定位问题。因此在使用EventBus的模块,若是在回调上有环路,并且回调方法复杂到了必定程度的话,就要考虑把接收事件专门封装成一个子模块,同时考虑避免出现事件环路。
固然,EventBus并非重构代码的惟一之选。做为观察者模式的“同门师兄弟”——RxJava,做为功能更为强大的响应式编程框架,能够轻松实现EventBus的事件总线功能(RxBus)。但毕竟大型项目要接入RxJava的成本高,复杂的操做符须要开发者投入更多的时间去学习。因此想在成熟的项目中快速地重构、解耦模块,EventBus依旧是咱们的不二之选。
本文总结了EventBus 3的使用方法,运行原理和一些新特性,让你们能直观地看到这个组件的优势和缺点,同时让你们在考虑是否在项目中引入EventBus时内心有个底。最后感谢Markus Junginger大神开源了如此实用的组件,以及组内同事在笔者探究EventBus原理时提供的帮助,但愿你们在看完本文后都能有所收获,成为NB的Android开发老司机。
更多精彩内容欢迎关注Bugly的微信公众帐号:
腾讯 Bugly 是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的状况以及解决方案。智能合并功能帮助开发同窗把天天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同窗定位到出问题的代码行,实时上报能够在发布后快速的了解应用的质量状况,适配最新的 iOS, Android 官方操做系统,鹅厂的工程师都在使用,快来加入咱们吧…