自己动手写事件总线(EventBus)

时间:2020-12-26 09:25:11

本文由云+社区发表

事件总线核心逻辑的实现。

EventBus的作用

Android中存在各种通信场景,如Activity之间的跳转,ActivityFragment以及其他组件之间的交互,以及在某个耗时操作(如请求网络)之后的callback回调等,互相之之间往往需要持有对方的引用,每个场景的写法也有差异,导致耦合性较高且不便维护。以ActivityFragment的通信为例,官方做法是实现一个接口,然后持有对方的引用,再强行转成接口类型,导致耦合度偏高。再以Activity的返回为例,一方需要设置setResult,而另一方需要在onActivityResult做对应处理,如果有多个返回路径,代码就会十分臃肿。而SimpleEventBus(本文最终实现的简化版事件总线)的写法如下:


public class MainActivity extends AppCompatActivity { TextView mTextView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mTextView = findViewById(R.id.tv_demo); mTextView.setText("MainActivity"); mTextView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(MainActivity.this, SecondActivity.class); startActivity(intent); } }); EventBus.getDefault().register(this); } @Subscribe(threadMode = ThreadMode.MAIN) public void onReturn(Message message) { mTextView.setText(message.mContent); } @Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); } }

来源Activity


public class SecondActivity extends AppCompatActivity { TextView mTextView; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mTextView = findViewById(R.id.tv_demo); mTextView.setText("SecondActivity,点击返回"); mTextView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Message message = new Message(); message.mContent = "从SecondActivity返回"; EventBus.getDefault().post(message); finish(); } }); } }

效果如下:

自己动手写事件总线(EventBus)

似乎只是换了一种写法,但在场景愈加复杂后,EventBus能够体现出更好的解耦能力。

背景知识

主要涉及三方面的知识:

  1. 观察者模式(or 发布-订阅模式)

  2. Android消息机制

  3. Java并发编程

本文可以认为是greenrobot/EventBus这个开源库的源码阅读指南,笔者在看设计模式相关书籍的时候了解到这个库,觉得有必要实现一下核心功能以加深理解。

实现过程

EventBus的使用分三个步骤:注册监听、发送事件和取消监听,相应本文也将分这三步来实现。

注册监听

定义一个注解:


@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Subscribe { ThreadMode threadMode() default ThreadMode.POST; }

greenrobot/EventBus还支持优先级和粘性事件,这里只支持最基本的能力:区分线程,因为如更新UI的操作必须放在主线程。ThreadMode如下:


public enum ThreadMode { MAIN, // 主线程 POST, // 发送消息的线程 ASYNC // 新开一个线程发送 }

在对象初始化的时候,使用register方法注册,该方法会解析被注册对象的所有方法,并解析声明了注解的方法(即观察者),核心代码如下:


public class EventBus { ... public void register(Object subscriber) { if (subscriber == null) { return; } synchronized (this) { subscribe(subscriber); } } ... private void subscribe(Object subscriber) { if (subscriber == null) { return; } // TODO 巨踏马难看的缩进 Class<?> clazz = subscriber.getClass(); while (clazz != null && !isSystemClass(clazz.getName())) { final Method[] methods = clazz.getDeclaredMethods(); for (Method method : methods) { Subscribe annotation = method.getAnnotation(Subscribe.class); if (annotation != null) { Class<?>[] paramClassArray = method.getParameterTypes(); if (paramClassArray != null && paramClassArray.length == 1) { Class<?> paramType = convertType(paramClassArray[0]); EventType eventType = new EventType(paramType); SubscriberMethod subscriberMethod = new SubscriberMethod(method, annotation.threadMode(), paramType); realSubscribe(subscriber, subscriberMethod, eventType); } } } clazz = clazz.getSuperclass(); } } ... private void realSubscribe(Object subscriber, SubscriberMethod method, EventType eventType) { CopyOnWriteArrayList<Subscription> subscriptions = mSubscriptionsByEventtype.get(subscriber); if (subscriptions == null) { subscriptions = new CopyOnWriteArrayList<>(); } Subscription subscription = new Subscription(subscriber, method); if (subscriptions.contains(subscription)) { return; } subscriptions.add(subscription); mSubscriptionsByEventtype.put(eventType, subscriptions); } ... }

执行过这些逻辑后,该对象所有的观察者方法都会被存在一个Map中,其Key是EventType,即观察事件的类型,Value是订阅了该类型事件的所有方法(即观察者)的一个列表,每个方法和对象一起封装成了一个Subscription类:


public class Subscription { public final Reference<Object> subscriber; public final SubscriberMethod subscriberMethod; public Subscription(Object subscriber, SubscriberMethod subscriberMethod) { this.subscriber = new WeakReference<>(subscriber);// EventBus3 没用弱引用? this.subscriberMethod = subscriberMethod; } @Override public int hashCode() { return subscriber.hashCode() + subscriberMethod.methodString.hashCode(); } @Override public boolean equals(Object obj) { if (obj instanceof Subscription) { Subscription other = (Subscription) obj; return subscriber == other.subscribe && subscriberMethod.equals(other.subscriberMethod); } else { return false; } } }

如此,便是注册监听方法的核心逻辑了。

消息发送

消息的发送代码很简单:


public class EventBus { ... private EventDispatcher mEventDispatcher = new EventDispatcher(); private ThreadLocal<Queue<EventType>> mThreadLocalEvents = new ThreadLocal<Queue<EventType>>() { @Override protected Queue<EventType> initialValue() { return new ConcurrentLinkedQueue<>(); } }; ... public void post(Object message) { if (message == null) { return; } mThreadLocalEvents.get().offer(new EventType(message.getClass())); mEventDispatcher.dispatchEvents(message); } ... }

比较复杂一点的是需要根据注解声明的线程模式在对应的线程进行发布:


public class EventBus { ... private class EventDispatcher { private IEventHandler mMainEventHandler = new MainEventHandler(); private IEventHandler mPostEventHandler = new DefaultEventHandler(); private IEventHandler mAsyncEventHandler = new AsyncEventHandler(); void dispatchEvents(Object message) { Queue<EventType> eventQueue = mThreadLocalEvents.get(); while (eventQueue.size() > 0) { handleEvent(eventQueue.poll(), message); } } private void handleEvent(EventType eventType, Object message) { List<Subscription> subscriptions = mSubscriptionsByEventtype.get(eventType); if (subscriptions == null) { return; } for (Subscription subscription : subscriptions) { IEventHandler eventHandler = getEventHandler(subscription.subscriberMethod.threadMode); eventHandler.handleEvent(subscription, message); } } private IEventHandler getEventHandler(ThreadMode mode) { if (mode == ThreadMode.ASYNC) { return mAsyncEventHandler; } if (mode == ThreadMode.POST) { return mPostEventHandler; } return mMainEventHandler; } }// end of the class ... }

三种线程模式分别如下,DefaultEventHandler(在发布线程执行观察者放方法):


public class DefaultEventHandler implements IEventHandler { @Override public void handleEvent(Subscription subscription, Object message) { if (subscription == null || subscription.subscriber.get() == null) { return; } try { subscription.subscriberMethod.method.invoke(subscription.subscriber.get(), message); } catch (IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); } } }

MainEventHandler(在主线程执行):


public class MainEventHandler implements IEventHandler { private Handler mMainHandler = new Handler(Looper.getMainLooper()); DefaultEventHandler hanlder = new DefaultEventHandler(); @Override public void handleEvent(final Subscription subscription, final Object message) { mMainHandler.post(new Runnable() { @Override public void run() { hanlder.handleEvent(subscription, message); } }); } }

AsyncEventHandler(新开一个线程执行):


public class AsyncEventHandler implements IEventHandler { private DispatcherThread mDispatcherThread; private IEventHandler mEventHandler = new DefaultEventHandler(); public AsyncEventHandler() { mDispatcherThread = new DispatcherThread(AsyncEventHandler.class.getSimpleName()); mDispatcherThread.start(); } @Override public void handleEvent(final Subscription subscription, final Object message) { mDispatcherThread.post(new Runnable() { @Override public void run() { mEventHandler.handleEvent(subscription, message); } }); } private class DispatcherThread extends HandlerThread { // 关联了AsyncExecutor消息队列的Handle Handler mAsyncHandler; DispatcherThread(String name) { super(name); } public void post(Runnable runnable) { if (mAsyncHandler == null) { throw new NullPointerException("mAsyncHandler == null, please call start() first."); } mAsyncHandler.post(runnable); } @Override public synchronized void start() { super.start(); mAsyncHandler = new Handler(this.getLooper()); } } }

以上便是发布消息的代码。

注销监听

最后一个对象被销毁还要注销监听,否则容易导致内存泄露,目前SimpleEventBus用的是WeakReference,能够通过GC自动回收,但不知道greenrobot/EventBus为什么没这样实现,待研究。注销监听其实就是遍历Map,拿掉该对象的订阅即可:


public class EventBus { ... public void unregister(Object subscriber) { if (subscriber == null) { return; } Iterator<CopyOnWriteArrayList<Subscription>> iterator = mSubscriptionsByEventtype.values().iterator(); while (iterator.hasNext()) { CopyOnWriteArrayList<Subscription> subscriptions = iterator.next(); if (subscriptions != null) { List<Subscription> foundSubscriptions = new LinkedList<>(); for (Subscription subscription : subscriptions) { Object cacheObject = subscription.subscriber.get(); if (cacheObject == null || cacheObject.equals(subscriber)) { foundSubscriptions.add(subscription); } } subscriptions.removeAll(foundSubscriptions); } // 如果针对某个Event的订阅者数量为空了,那么需要从map中清除 if (subscriptions == null || subscriptions.size() == 0) { iterator.remove(); } } } ... }

以上便是事件总线最核心部分的代码实现,完整代码见vimerzhao/SimpleEventBus,后面发现问题更新或者进行升级也只会改动仓库的代码。

局限性

由于时间关系,目前只研究了EventBus的核心部分,还有几个值得深入研究的点,在此记录一下,也欢迎路过的大牛指点一二。

性能问题

实际使用时,注解和反射会导致性能问题,但EventBus3已经通过Subscriber Index基本解决了这一问题,实现也非常有意思,是通过注解处理器(Annotation Processor)把耗时的逻辑从运行期提前到了编译期,通过编译期生成的索引来给运行期提速,这也是这个名字的由来。

可用性问题

如果订阅者很多会不会影响体验,毕竟原始的方法是点对点的消息传递,不会有这种问题,如果部分订阅者尚未初始化怎么办。等等。目前EventBus3提供了优先级和粘性事件的属性来进一步满足开发需求。但是否彻底解决问题了还有待验证。

跨进程问题

EventBus是进程内的消息管理机制,并且从开源社区的反馈来看这个项目是非常成功的,但稍微有点体量的APP都做了进程拆分,所以是否有必要支持多进程,能否在保证性能的情况下提供同等的代码解耦能力,也值得继续挖掘。目前有lsxiao/ApolloXiaofei-it/HermesEventBus等可供参考。

参考

此文已由作者授权腾讯云+社区发布


自己动手写事件总线(EventBus)的更多相关文章

  1. vue*事件总线eventBus的简单理解和使用

    公共事件总线eventBus的实质就是创建一个vue实例,通过一个空的vue实例作为桥梁实现vue组件间的通信.它是实现非父子组件通信的一种解决方案. 用法如下: 第一步:项目中创建一个js文件(我通 ...

  2. C&num; 事件总线 EventBus

    1. 引言 事件总线这个概念对你来说可能很陌生,但提到观察者(发布-订阅)模式,你也许就很熟悉.事件总线是对发布-订阅模式的一种实现.它是一种集中式事件处理机制,允许不同的组件之间进行彼此通信而又不需 ...

  3. Guava&colon; 事件总线EventBus

    EventBus 直译过来就是事件总线,它使用发布订阅模式支持组件之间的通信,不需要显式地注册回调,比观察者模式更灵活,可用于替换Java中传统的事件监听模式,EventBus的作用就是解耦,它不是通 ...

  4. 【bird-java】分布式服务间的事件总线EventBus

    什么是EventBusEventBus是对发布-订阅模式的一种实现.其以一种非常优雅的方式实现了组件间的解耦与通信,在Android开发.DDD等领域都有非常广泛的应用. 事件流大致如下: Produ ...

  5. 事件总线EventBus

    什么是事件总线管理? 将事件放到队列里,用于管理和分发: 保证应用的各个部分之间高效的通信及数据,事件分发: 模块间解耦: 什么是EventBus? EventBus是发布/订阅的事件总线.Event ...

  6. Android事件总线EventBus详解

    顾名思义,AndroidEventBus是一个Android平台的事件总线框架,它简化了Activity.Fragment.Service等组件之间的交互,很大程度上降低了它们之间的耦合,使我们的代码 ...

  7. ASP&period;NET Core基于微软微服务eShopOnContainer事件总线EventBus的实现

    这个EventBus的实现是基于微软微服务https://github.com/dotnet-architecture/eShopOnContainers项目的,我把它从项目中抽离出来,打包成nuge ...

  8. 事件总线 EventBus

    661. .net中事件模型很优雅的实现了观察者模式,同时被大量的使用在各种框架中. [2016-04-30 10:52:42]662. Prism框架中实现了一个典型的EventAggregator ...

  9. 【java】简单的事件总线EventBus

    public class EventBus { private static Map<String, EventListener> eventListeners = new HashMap ...

随机推荐

  1. 无向图的DFS遍历(方法之一)

    如果看不懂辅助解释在后面第点 1.录入方式: 输入 u - v  表示一边的2个端点 2.存储结构 struct edge { int from; int to; int next; } e[MAXN ...

  2. &lbrack;资料&rsqb;自动化e2e测试 -- WebDriverJS,Jasmine和Protractor

    1. http://sentsin.com/web/658.html 2. http://www.tuicool.com/articles/AnE3Mb 3. http://www.doc88.com ...

  3. UML--核心元素之包

    包是一种容器,如同文件夹一样. 包是UML非常常用的一个元素,它最主要的作用就是容纳并为其他元素分类.包可以容纳用例.业务实体.类图等,也包含子包. 分包的原则 1.如果将元素分为三个包A.B.C,那 ...

  4. Aptana Studio 3 官方汉化包汉化

    Babel Language Pack Update Site for Helios This URL is an Eclipse software repository:http://downloa ...

  5. Oracle数据库的锁类型

    Oracle数据库的锁类型 博客分类: oracle   Oracle数据库的锁类型 根据保护的对象不同,Oracle数据库锁可以分为以下几大类:DML锁(data   locks,数据锁),用于保护 ...

  6. P&sol;Invoke与逆向P&sol;Invoke

    1.在在 C# 中通过 P/Invoke 调用Win32 DLL这篇文中,详细介绍了P/Invoke的基本知识以及使用. 2.InAttribute和OutAttribute特性与C#中ref和out ...

  7. 推荐系列:最小与最大&lbrack;DP&plus;余式定理&rsqb;

    最小与最大 [问题描述] 做过了乘积最大这道题,相信这道题也难不倒你. 已知一个数串,可以在适当的位置加入乘号(设加了k个,当然也可不加,即分成k+1个部分),设这k+1个部分的乘积(如果k=0,则乘 ...

  8. mac下安装gradle

    安装 gradle brew install gradle 配置Path环境 vim .bash_profile // 写入 export PATH="${PATH}:/usr/local/ ...

  9. 文本框监听事件blur&lpar;&rpar;的简单使用

    场景描述:在做编辑功能的时候,经常要判断编码,或者密码之类的是否已经被使用,以前自己做的时候,经常都是在提交了之后才判断的,到现在,才发现,这样做的用户体验不好,完美一点的做法就是当此文本框失去焦点的 ...

  10. Syncthing – 数据同步利器---自己的网盘,详细安装配置指南,内网使用,发现服务器配置

    简介: 无论办公.文件共享.团队协作还是家庭照片.视频.音乐.高清电影的存储,我们常常都有文件同步和存储的需求.但随着国内各大网盘的花式阵亡或限速,早已没什么好选择了.好吧,我已经转战使用onedri ...