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

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

本文由云+社区发表

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

<!--more-->

EventBus的作用

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


public class MainActivity extends AppCompatActivity {TextView mTextView;@Overrideprotected 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() {@Overridepublic 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);}@Overrideprotected void onDestroy() {super.onDestroy();EventBus.getDefault().unregister(this);}}

来源Activity


public class SecondActivity extends AppCompatActivity {TextView mTextView;@Overrideprotected 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() {@Overridepublic void onClick(View view) {Message message = new Message();message.mContent = "从SecondActivity返回";EventBus.getDefault().post(message);finish();}});}}

效果如下:

似乎只是换了一种写法,但在场景愈加复杂后,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;}@Overridepublic int hashCode() {return subscriber.hashCode() + subscriberMethod.methodString.hashCode();}@Overridepublic 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>>() {@Overrideprotected 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 {@Overridepublic 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();@Overridepublic void handleEvent(final Subscription subscription, final Object message) {mMainHandler.post(new Runnable() {@Overridepublic 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();}@Overridepublic void handleEvent(final Subscription subscription, final Object message) {mDispatcherThread.post(new Runnable() {@Overridepublic void run() {mEventHandler.handleEvent(subscription, message);}});}private class DispatcherThread extends HandlerThread {// 关联了AsyncExecutor消息队列的HandleHandler 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);}@Overridepublic 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/Apollo和Xiaofei-it/HermesEventBus等可供参考。

参考

  • greenrobot/EventBus

  • hehonghui/AndroidEventBus

  • 《Android开发艺术探索》第十章

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


转载于:https://my.oschina.net/qcloudcommunity/blog/2995084

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/388727.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

viz::viz3d报错_我可以在Excel中获得该Viz吗?

viz::viz3d报错Have you ever found yourself in the following situation?您是否遇到以下情况&#xff1f; Your team has been preparing and working tireless hours to create and showcase the end product — an interactive visual dashboard. It’s a culmination of…

java 添加用户 数据库,跟屌丝学DB2 第二课 建立数据库以及添加用户

在安装DB2 之后&#xff0c;就可以在 DB2 环境中创建自己的数据库。首先考虑数据库应该使用哪个实例。实例(instance) 提供一个由数据库管理配置(DBM CFG)文件控制的逻辑层&#xff0c;可以在这里将多个数据库分组在一起。DBM CFG 文件包含一组 DBM CFG 参数&#xff0c;可以使…

iphone视频教程

公开课介绍 本课程共28集 翻译至第15集 网易正在翻译16-28集 敬请关注 返回公开课首页 一键分享&#xff1a;  网易微博开心网豆瓣网新浪微博搜狐微博腾讯微博邮件 讲师介绍 名称&#xff1a;Alan Cannistraro 课程介绍 如果你对iPhone Development有兴趣&#xff0c;以下是入…

在Python中有效使用JSON的4个技巧

Python has two data types that, together, form the perfect tool for working with JSON: dictionaries and lists. Lets explore how to:Python有两种数据类型&#xff0c;它们一起构成了使用JSON的理想工具&#xff1a; 字典和列表 。 让我们探索如何&#xff1a; load a…

Vlan中Trunk接口配置

Vlan中Trunk接口配置 参考文献&#xff1a;HCNA网络技术实验指南 模拟器&#xff1a;eNSP 实验环境&#xff1a; 实验目的&#xff1a;掌握Trunk端口配置 掌握Trunk端口允许所有Vlan配置方法 掌握Trunk端口允许特定Vlan配置方法 实验拓扑&#xff1a; 实验IP地址 &#xff1a;…

django中的admin组件

Admin简介&#xff1a; Admin:是django的后台 管理的wed版本 我们现在models.py文件里面建几张表&#xff1a; class Author(models.Model):nid models.AutoField(primary_keyTrue)namemodels.CharField( max_length32)agemodels.IntegerField()# 与AuthorDetail建立一对一的关…

虚拟主机创建虚拟lan_创建虚拟背景应用

虚拟主机创建虚拟lanThis is the Part 2 of the MediaPipe Series I am writing.这是我正在编写的MediaPipe系列的第2部分。 Previously, we saw how to get started with MediaPipe and use it with your own tflite model. If you haven’t read it yet, check it out here.…

.net程序员安全注意代码及服务器配置

概述 本人.net架构师&#xff0c;软件行业为金融资讯以及股票交易类的软件产品设计开发。由于长时间被黑客攻击以及骚扰。从事高量客户访问的服务器解决架构设计以及程序员编写指导工作。特此总结一些.net程序员在代码编写安全以及服务器设置安全常用到的知识。希望能给对大家…

接口测试框架2

现在市面上做接口测试的工具很多&#xff0c;比如Postman&#xff0c;soapUI, JMeter, Python unittest等等&#xff0c;各种不同的测试工具拥有不同的特色。但市面上的接口测试工具都存在一个问题就是无法完全吻合的去适用没一个项目&#xff0c;比如数据的处理&#xff0c;加…

python 传不定量参数_Python中的定量金融

python 传不定量参数The first quantitative class for vanilla finance and quantitative finance majors alike has to do with the time value of money. Essentially, it’s a semester-long course driving notions like $100 today is worth more than $100 a year from …

雷军宣布红米 Redmi 品牌独立,这对小米意味着什么?

雷锋网消息&#xff0c;1 月 3 日&#xff0c;小米公司宣布&#xff0c;将在 1 月 10 日召开全新独立品牌红米 Redmi 发布会。从小米公布的海报来看&#xff0c;Redmi 品牌标识出现的倒影中&#xff0c;有 4800 的字样&#xff0c;这很容易让人联想起此前小米总裁林斌所宣布的 …

JAVA的rotate怎么用,java如何利用rotate旋转图片_如何在Java中旋转图形

I have drawn some Graphics in a JPanel, like circles, rectangles, etc.But I want to draw some Graphics rotated a specific degree amount, like a rotated ellipse. What should I do?解决方案If you are using plain Graphics, cast to Graphics2D first:Graphics2D …

贝叶斯 朴素贝叶斯_手动执行贝叶斯分析

贝叶斯 朴素贝叶斯介绍 (Introduction) Bayesian analysis offers the possibility to get more insights from your data compared to the pure frequentist approach. In this post, I will walk you through a real life example of how a Bayesian analysis can be perform…

西工大java实验报告给,西工大数字集成电路实验 实验课6 加法器的设计

西工大数字集成电路实验练习六 加法器的设计一、使用与非门(NAND)、或非门(NOR)、非门(INV)等布尔逻辑器件实现下面的设计。1、仿照下图的全加器&#xff0c;实现一个N位的减法器。要求仿照图1画出N位减法器的结构。ABABABAB0123图1 四位逐位进位加法器的结构2、根据自己构造的…

DS二叉树--二叉树之数组存储

二叉树可以采用数组的方法进行存储&#xff0c;把数组中的数据依次自上而下,自左至右存储到二叉树结点中&#xff0c;一般二叉树与完全二叉树对比&#xff0c;比完全二叉树缺少的结点就在数组中用0来表示。&#xff0c;如下图所示 从上图可以看出&#xff0c;右边的是一颗普通的…

VS IIS Express 支持局域网访问

使用Visual Studio开发Web网页的时候有这样的情况&#xff1a;想要在调试模式下让局域网的其他设备进行访问&#xff0c;以便进行测试。虽然可以部署到服务器中&#xff0c;但是却无法进行调试&#xff0c;就算是注入进程进行调试也是无法达到自己的需求&#xff1b;所以只能在…

构建图像金字塔_我们如何通过转移学习构建易于使用的图像分割工具

构建图像金字塔Authors: Jenny Huang, Ian Hunt-Isaak, William Palmer作者&#xff1a; 黄珍妮 &#xff0c; 伊恩亨特伊萨克 &#xff0c; 威廉帕尔默 GitHub RepoGitHub回购 介绍 (Introduction) Training an image segmentation model on new images can be daunting, es…

PHP mongodb运用,MongoDB在PHP下的应用学习笔记

1、连接mongodb默认端口是&#xff1a;27017&#xff0c;因此我们连接mongodb&#xff1a;$mongodb new Mongo(localhost) 或者指定IP与端口 $mongodb new Mongo(192.168.127.1:27017) 端口可改变若mongodb开启认证&#xff0c;即--auth,则连接为&#xff1a; $mongodb new …

SpringBoot项目打war包部署Tomcat教程

一、简介 正常来说SpringBoot项目就直接用jar包来启动&#xff0c;使用它内部的tomcat实现微服务&#xff0c;但有些时候可能有部署到外部tomcat的需求&#xff0c;本教程就讲解一下如何操作 二、修改pom.xml 将要部署的module的pom.xml文件<packaging>节点设置为war <…

关于如何使用xposed来hook微信软件

安卓端 难点有两个 收款码的生成和到帐监听需要源码加 2442982910转载于:https://www.cnblogs.com/ganchuanpu/p/10220705.html