为什么要使用EventBus
在Android开发过程中经常有这样的场景–>栈顶Activity需要关闭非栈顶的Activity,或调用其中的某些方法。
案例:我有三个Activity,主界面Activity,个人中心Activity,登录界面Activity,主界面打开了个人中心页面,个人中心页面有个退出登录的功能,点击后退出登录关闭主界面Activity和当前Activity,打开登录页面.
这只是个简单的案例,当然关闭当前Activity和打开登录页面的Activity都是没什么难度的东西。
我们来讨论主界面关闭的一些实现方案。
得到主界面Activity的对象。这边可能需要某个静态方法来持有这个对象,以前见过一种写法是写一个ActivityManager来持有这些Activity对象,然后手动管理这些Activity。
发送一个广播通知主界面Activity.
在当前Activity销毁时传一个resultCode,主界面看到这个resultCode来判断是否是否要做关闭操作。
暂时能想到的就这三种实现方案,分析一下三种实现:
第三种实现方案并不适用于比如我现在是主界面–>个人主页–>个人中心,做第三种方案的话代码写到哭晕在厕所,并且第三种方案代码写起来会很复杂,页面可能还没关掉就把自己绕晕了(个人想法,不推荐!)
第二种实现方案:发送一个广播做通知,其实是一个蛮不错的选择,发送广播然后实现广播类做回调通知。但是如果现在需求是对一个Fragment做通知。这个时候广播就派不上用场了。毕竟广播只能再Activity中注册发送广播。
第一种实现方案:这种方案在以前还是蛮常见的,以前学Android的时候会看到一些大牛们,自己写一个ActivityManager用来存放Activity对象做生命周期管理。但是这样做其实是会有问题的。因为持有的是Activity的对象,这就导致了Activity只能手动销毁。每次写关闭页面的时候都要调用这个方法来销毁Activity,不然分分钟就OOM了。还有另外一个缺点就是需要遍历整个栈来确定存活的Activity对象。找到你要找的Activity,然后调用其中的方法。如果是要销毁则还要将它移除栈中。(心很累)
想想这些方法真的都好难用呀。
这个时候就需要来个上帝来管理这些东西了。EventBus一脸害羞的跳出来了。一种全局的观察者模式,你可以把你的愿望(Event)请求post给这位上帝,它提供多线程的处理方案,来让你的(愿望)在你指定的地方实现。
EventBus使用
在Github上可以看到gradle所需配置
在gradle文件做上如上配置就可以了
分三个步骤
- 定义一个作为通知的实体类Event;
- 在需要订阅的地方做注册和取消注册,写一个订阅方法(方法需要写注解 @Subscribe(threadMode = ThreadMode.XXX))
- 在任意地方调用EventBus.getDefault().post(Event());
简单代码如下:
在MainActivity注册和解除注册,并写出订阅方法
|
|
在UserInfoActivity发送通知
|
|
LoginActivity
|
|
实体类
|
|
运行结果
如此简单便捷的使用方式,确实是做消息通知的不二选择。不过在注册过EventBus的类中,千万别忘了在结束的时候取消注册,不然会抛运行时异常。因为你注册了某个对象却不知道它什么时候被销毁了,回调方法的时候肯定会空针,这种回调解除注册在某些注册类的生命周期短,而维护类生命周期长的情况下经常能使用到~~这么好用的东西还是需要去了解一下其内部实现的。而且EventBus内部实现其实并不复杂。
EventBus源码解析
注册EventBus的时候需要执行,EventBus.getDefault().register(this);
我们先从getDefault方法入手,看下里面做了些什么
|
|
大致代码如下,单例模式获取一个EventBus对象,保证所有的EventBus对象是同一个。
进入 EventBus构造看一下
|
|
两个构造方法,一个是无参的public的方法,另外一个是内部方法,带参数的。
第一个方法默认调用有参数的构造方法,来生成一个EventBus对象。
这边DEFAULT_BUILDER声明如下
|
|
存放着一个默认的EventBusBuilder对象;
进入EventBusBuilder中看
|
|
里面是一些EventBus的基础配置。打印异常信息,发送未被订阅的消息等。
特别需要注意的是 ignoreGeneratedIndex 这个变量是用来确定EventBus用什么方式来获取订阅方法。
true代表的是不优先使用索引,用反射的方式,false代表的是优先使用索引。默认是false。
这边有个addIndex方法是用来添加订阅方法索引的,这是3.0版本的新特性。
在EventBus带参数的构造函数里面初始化了 SubscriberMethodFinder 这个类,三个参数分别为索引列表,
是否严格方法定义,是否无视索引。
getDefault方法总结,该方法返回一个单例的EventBus对象,如果对象没被创建就创建一个默认的EventBus对象。
接下来从注册方法下手
|
|
首先是通过一个findSubscriberMethods方法找到了一个订阅者中的所有订阅方法,返回一个 List
findSubscriberMethods这个方法声明SubscriberMethodFinder这个类中,类中声明了一个ConcurrentHashMap用来做SubscriberMethod的缓存。
回到findSubscriberMethods中,首先是通过类来当key,查找在当前缓存中是否存在这个类的订阅方法列表。有就直接return 缓存中的列表。
没有就进入下一步,这边有两种方式来获取类中的订阅方法通过ignoreGeneratedIndex来确定获取订阅方法的方式为true时会使用反射方法获取订阅者的事件处理函数,为false时会优先使用subscriber Index(订阅方法索引)生成的SubscriberInfo来获取订阅者的事件处理函数(减少反射耗时),默认false。(使用订阅方法索引需自己构建一个EventBus对象,将在后面提及,目前的使用并不能用到索引)
进入findUsingInfo这个方法
|
|
- 从FindState池——FIND_STATE_POOL中获取一个FindState对象。(SubscriberMethodFinder这个类中声明了一个FIND_STATE_POOL的对象池,这个用来存放空(并非NULL)的FIND_STATE对象,减少创建对象的花销)
- 该方法会将findState.clazz域中使用了@subscribe标注、方法中只有一个参数、且方法修饰符为public的方法,创建一个SubscriberMethod对象,并添加到findState的List
集合中。 - 将findState.clazz域更新为clazz = clazz.getSuperclass(); 如果该超类名字以java. javax. android.开头则clazz变成null;不再往上寻找父类;
- 拷贝一份findState的List
集合并返回,最后回收findState对象,回收的只是释放这个对象内部的变量的资源占用,但这个对象还是存在的,放回对象池中,下次可以再取出来用;
看一下如何用反射获取订阅方法。
|
|
- 获取订阅类声明的所有方法; 然后对获取到的方法全部遍历一遍
- 获取方法的修饰符:即方法前面的public、private等关键字。
- 如果该类方法使用了@subscribe标注、方法中只有一个参数、且方法修饰符为public。findState.checkAdd(method, eventType) 如果之前没有存在过则返回true
- 判断@Subscribe标注中的threadMode对应的值,默认模式ThreadMode.POSTING
- 创建一个SubscriberMethod对象,该对象很简单就是保存有方法、方法参数类型、线程模式、订阅的优先级、sticky标志位。与Retrofit类似只是这里创建了一个SubscriberMethod对象。并将该对象添加到FindSate的List
集合中。
回到注册方法,来看一下subscribe方法的实现
|
|
- 获取方法参数类型;注意:使用@Subscribe标注的方法有且仅有一个参数
- 利用订阅者对象及其事件处理方法构建一个Subscription对象,该对象存储有Object、SubscriberMethod对象
- 从subscriptionsByEventType集合中获取当前事件对应的Subscription对象集合; 如果得到的集合为空则创建一个这样的集合,并将刚创建的Subscription对象添加进subscriptionsByEventType集合中;如果得到的集合不为空且刚创建的Subscription对象已经存在该集合中则抛出异常,即同一个对象不能注册两次!
- 将第二步创建的Subscription对象按照优先级存入Subscription对象集合中,该集合中的元素都是按照优先级从高到低存放.
- 以subscriber对象为键,从typesBySubscriber获取该对象对应的接收事件类型集合,没有则创建一个这样的集合,然后当前事件类型添加到该集合中,最后将整个集合添加进typesBySubscriber集合中;有则直接添加到接收事件类型集合中;
- 该值默认为false,除非在注册事件方法时使用了如下的标注@Subscribe(sticky = true);那么就会执行到这里。stickyEvent也是EventBus3.0的一大特点,该类事件一旦发送给EventBus,那么EventBus就会将它存入Map
, Object> stickyEvents集合中,key事件类型,value事件实例;每个类型事件对应最新的实例。当订阅对象的某个事件处理方法使用了@Subscribe(sticky = true)标注,EventBus一旦对订阅者完成了注册任务之后,立即将stickyEvents集合中的匹配事件发送给该事件处理进行处理!
接下来看post方法
|
|
- 获取当前线程对应的一个PostingThreadState()对象;回顾一下我们在EventBus中创建的如下域
|
|
ThreadLocal类型的特点是当调用currentPostingThreadState.get()方法的时候,会返回当前线程所持有的一个 PostingThreadState对象;在不同的线程中执行同样一行代码它们得到的对象是不同的。PostingThreadState也很简单,就是定义了一堆数据,没有任何方法。下面就是它的所有源码
|
|
- 向PostingThreadState的事件队列中添加一个事件
- 从PostingThreadState的事件队列——eventQueue中移出一个事件,并调用postSingleEvent方法进行派送
postSingleEvent方法
|
|
- eventInheritance该标志位默认为true,表示只要 满足订阅事件是该事件的父类或者实现了该事件同样接口或接口父类 中的任何一个条件的订阅者都会来处理该事件。
- 该方法从名字来看就是获取eventClass的所有父类往上都能追溯到Object、和所有实现的接口、以及接口父类;EventBus进行了优化采用了缓存机制Map
, List >> eventTypesCache。 - 将eventClass所有的关联类都通过postSingleEventForEventType进行推送;下面跟着就会分析该方法
- 如果该事件没有推送成功,即没有事件处理器来处理这类事件;系统会尝试发送一个post(new NoSubscriberEvent(this, event))事件
postSingleEventForEventType方法
|
|
- 获取原始事件的关联类对应的所有Subscription对象
- 将上述Subscription对象集合进行遍历,使用postToSubscription方法处理原始事件
postToSubscription方法
|
|
- POSTING直接在当前线程执行,调用invokeSubscriber方法
- MAIN即UI线程执行:如果当前线程就是UI线程则直接调用invokeSubscriber方法,否则将任务交给mainThreadPoster——HandlerPoster(this, Looper.getMainLooper(), 10)对象异步执行,最终会调用invokeSubscriber(PendingPost pendingPost)方法;
- BACKGROUND背景线程执行:其实就在非UI线程中执行,如果当前线程是非UI线程则直接调用invokeSubscriber方法,否则将任务交给backgroundPoster——BackgroundPoster(this)对象异步执行,最终会调用invokeSubscriber(PendingPost pendingPost)方法;
- ASYNC:将任务交给 asyncPoster—— AsyncPoster(this)对象异步执行,最终会调用invokeSubscriber(PendingPost pendingPost)方法;
- 就是根据不同的Post模式,选择不同的方式反射执行方法。
接下来看取消注册的方法
|
|
- 从typesBySubscriber获取该对象接收的事件类型集合;
- 对得到的接收事件类型集合中的每个事件类型调用unsubscribeByEventType进行处理;跟着我们就分析该方法
- 该对象从typesBySubscriber集合中移除;
|
|
1、从subscriptionsByEventType集合中获取该事件类型对应的Subscription集合
2、如果集合中的元素——Subscription的subscriber域等于目标subscriber,则将该Subscription从subscriptionsByEventType集合中移除出去;
源码部分就讲解到这里
由于对注解不是很了解,所以在通过索引获取订阅方法的时候我也是一脸蒙逼,因为索引列表是在构造函数中生成的。但是addIndex从来没被调用过。我就很好奇索引列表是怎么来的,后面才明白是另外有方法来添加订阅者索引列表,下面我们来看一下如何使用索引。
索引的好处
EventBus提供了一个扩展包 EventBusAnnotationProcessor 这个方法可以在编译的时候通过注解,自动生成订阅方法索引列表的类。由于是在编译阶段就执行的,这样做可以避免反射获取所需的耗时。缺点可能是会占用一小部分内存。
如何使用索引
使用索引需要在项目gradle下添加
|
|
如下
|
|
然后在app的gradle中添加
其中eventBusIndex 是路径加类名,用来确定存放的位置,可以自己定义。
配置完之后先Rebuild一把
可以看到在debug目录下生成了这个EventBusTestIndex这个类。
类中方法如下
就是一个静态的Map对象用来存放所有订阅方法字典。然后提供一个get方法用来获取这个字典中的对象。
然后就是使用了
|
|
这边再贴一遍installDefaultEventBus这个方法的源码
|
|
可以看出这个方法用来自定义一个EventBusBuider对象配置到EventBus中去,并替换掉defaultInstance。
注意:这里需要在Default EventBus 对象还没生成的时候执行这句话。如果已经有默认的EventBus对象存在了再installDefaultEventBus会报错的。
所以我们需要在APP启动的时候把这个方法加进去,新建一个App类继承Application
|
|
AndroidManifest中别忘了把Application改成当前App
之后的方法获取就都是通过索引获取了。
总结
没有精确去分析所有的源码。这边大概做一下总结,整个流程大概如下。
1. 注册EventBus,EventBus会获取当前类中的订阅方法,包括方法的参数,类型,优先级等信息。在获取的方法中,经常会有缓存。如果在缓存中没有才去调用获取方法。获取订阅方法有两种方式,一种是反射,反射获取全部方法,找到加了@Subscribe注解并有一个参数的方法,另外一种是通过订阅方法索引来得到订阅方法。反射的效率较低,所以一般都是用订阅方法索引来获取,有些情况下找不到这个方法。它还是会走反射的途径。注册过后这个订阅方法就被存起来了。等待接受消息。(这边值得说一下在3.0以前是没有注解方法的那时候都是通过反射来获取和执行方法。而且方法必须以onEvent开头,分别为onEventMainThead,onEventBackground,onEvent,onEventAsync)
3. postEvent,会把这个Event加入消息队列,然后通过Event的类型信息来得到它的订阅方法,然后根据相应的订阅方式反射调用订阅方法。没有选择的Post一般都会在UI线程执行。如果当前post不是UI线程这边会用Handle的机制来让方法运行在UI线程。
4. 解除注册,将该对象从对象缓存列表中移除,获取当前对象的订阅列表,然后将其从订阅列表移除
By Xiaolong,每一天都值得被认真对待!