如何自己实现一个 EventBus

什么是 EventBus

EventBus 是一个基于观察者模式的事件发布/订阅框架,开发者可以通过极少的代码去实现组件,模块之间的通信,�而不需要以层层传递接口的形式去单独构建通信桥梁。从而降低因多重回调导致的模块间强耦合,同时避免产生大量内部类。它拥有使用方便,性能高,接入成本低和支持多线程的优点,实乃模块解耦、代码重构必备良药。

Android 中 提供了 Handler 来进行组件间的通信,而 Handler 在使用上有很多不便,EventBus �的出现完美的�解决了这些问题。

用了那么久 EventBus 所以我决定自己实现一个,正好最近项目开始使用 Kotlin 来写,所以本文中的代码和例子全部使用 Kotlin 来完成。

Github 地址:https://github.com/Werb/EventBusKotlin

EventBus 的原理

前面说了 EventBus 是基于观察者模式,核心是事件。通过事件的发布和订阅实现组件之间的通信,�EventBus 默认是一个单例存在,在 Java 中还需要使用 Synchronized 来保证线程安全。通俗来讲,EventBus 通过注册将所有订阅事件的方法储存在集合中,当有事件发布的时候,根据某些规则,匹配出符合条件的方法,调用执行,从而实现组件间的通信。

发布的事件相当于被观察者,注册的对象相当于观察者,被观察者和观察者是一对多的关系。当被观察者状态发生变化,即发布事件的时候,观察者对象将会得到通知�并作出响应,即执行对应的方法。

EventBus 具体实现

EventBus 最终的�目的,是在当有事件发生的时候,调用执行对应的方法,这里我们采用注解的方式来标记执行的方法,最终通过反射来调用。

Subscriber

Subscriber 是一个注解类,我们通过 @Subscriber关键字来标记方法。Subscriber 在声明的时候,有两个可选�参数,tag: Stringmode: ThreadMode,并且这两个参数都有自己的默认值。

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION,AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
annotation class Subscriber(val tag: String = DEFAULT_TAG, val mode: ThreadMode = ThreadMode.POST)
  • @Retention(AnnotationRetention.RUNTIME) 声明在运行时使用注解
  • AnnotationTarget.FUNCTION �声明注解时作用在方法上
  • AnnotationTarget.PROPERTY_GETTER 声明属性存在 GET
  • AnnotationTarget.PROPERTY_SETTER 声明属性存在 SET

tag

tag 的目的是为了�当有相同事件发生的时候,细化区分不同的事件。

例如我们定义了一个 SessionEvent 用于账户登录和登出时的信息传递,这时候我们就可以定义 loginlogout 两个 tag 来进行区分。当然你也完全可以定义两个 LoginEventLogoutEvent,这个字段的目的是为了可以更加方便灵活的操作代码。

tag 的默认值为 DEFAULT_TAG

mode

mode 的目的是为了�声明注解所标记的方法的执行环境。

mode 所代表的是 ThreadMode 类。这是一个枚举类,可选值为MAINPOSTBACKGROUND,�即响应 Event 事件时�,方法执行所在的线程。

  • MAIN 主线程 UI 线程
  • POST 事件发出所在的线程
  • BACKGROUND 子线程

mode 的默认值为 POST �。

IEvent

在这里,我�定义了�一个空的�接口 IEvent ,我们通过 EventBus 发出的事件类需要实现这个接口,同时在通过�注解定义事件执行方法的时候,需要将我们接收的某个事件类作为方法的参数,有且只有一个参数,它就是我们的被观察者�。

一个完整的例子:

/** EventBus 发送的事件类 这个类的目的用于用户登录登出的相关操作 */
class SessionEvent: IEvent
/** 登录成功时发送 Event */
EventBus.post(SessionEvent(), "login")
/** 登出成功时发送 Event */
EventBus.post(SessionEvent(), "logout")
/** 登录成功 默认在 POST 线程执行 */
@Subscriber(tag = "login")
private fun login(event: SessionEvent) {
    // do something
}

/** 登录成功 在 MAIN 线程执行,通常是一些 UI 上的操作 */
@Subscriber(tag = "logout", mode = ThreadMode.MAIN)
private fun logout(event: SessionEvent) {
    // do something
}

��前面说了�,我们需要将所有订阅事件的方法存储到一个集合中,当有事件�发出的时候,我们通过某些规则,匹配出符合条件的方法,调用执行。所以,�首先我们需要去定采用哪种集合来存储,存储时的规则是什么。

MutableMap

这里我们采用 MutableMap 来存储事件执行的方法。MutableMap 在 Kotlin 中表示为可变的,结构如下。

MutableMap<EventType, CopyOnWriteArrayList<Subscription>>

�EventType 是唯一key(下面会介绍),通过key找到对应的执行方法。
CopyOnWriteArrayList<Subscription>> 是�一个线程安全的 List ,前面我们说过了�被观察者和观察者是一对多的关系所以这里使用 List,Subscription事件执行方法的�包装类(下面会介绍)。

�EventType

�EventType 类包含两个字段,eventClass: Class<IEvent>tag: String

  • eventClass: Class<IEvent> 是我们通过 EventBus 发出的事件类

  • tag: String 是我们�发出事件类时所指定的 tag

internal class EventType(private var eventClass: Class<IEvent>, private var tag: String) {

    override fun equals(other: Any?): Boolean {
        // 比较内存引用地址,相同则返回 true
        if (this === other) {
            return true
        }

        // 判断是否为空,是否属于同一中类型
        if (other == null || (other.javaClass.name !== this.javaClass.name)) {
            return false
        }

        // 能执行到这里,说明 obj 和 this 同类且非 null
        val eventType = other as EventType
        val tagJudge = tag == eventType.tag
        val eventJudge = eventClass.name == eventType.eventClass.name

        return tagJudge && eventJudge
    }

    override fun hashCode(): Int {
        var hash = 7
        hash = hash * 31 + eventClass.hashCode()
        hash = hash * 31 + tag.hashCode()
        return hash
    }
}

通过这两个字段我们就可以确定一个唯一的key,准确的找到所对应的事件执行方法。

Subscription

Subscription 是事件执行方法的�包装类,它包含 subscriber: WeakReference<Any>targetMethod: MethodthreadMode: ThreadMode 这三个字段。

  • subscriber: WeakReference<Any> 这个就是我们的观察者,它可以是一个 ActivityFragment�Service,�我们注册了这个观察者到我们的 EventBus 中,当�被观察者产生变化的时候,观察者调用执行对应的方法,并且这里使用弱�引用来包装这个对象。
  • targetMethod: Method 事件调用执行的具体方法。
  • threadMode: ThreadMode 事件�方法执行时所在的环境。
internal class Subscription(val subscriber: WeakReference<Any>,
                            val targetMethod: Method,
                            val threadMode: ThreadMode) {

    override fun equals(other: Any?): Boolean {
        if (this === other) {
            return true
        }

        if (other == null || (other::class !== this::class)) {
            return false
        }

        val subscription = other as Subscription
        val judgeSubscriber = this.subscriber.get() === subscription.subscriber.get()
        val judgeMethod = this.targetMethod.name == subscription.targetMethod.name
        return judgeSubscriber && judgeMethod
    }

    override fun hashCode(): Int {
        var hash = 7
        hash = hash * 31 + subscriber.hashCode()
        hash = hash * 31 + targetMethod.hashCode()
        hash = hash * 31 + threadMode.hashCode()
        return hash
    }

}

好了,现在我们确定了 EventBus 的�内部存储结构,接下来我们就要说一下最关键的部分,EventBus 是如何注册 register 和发送 post 事件的,先看一下 EventBus 的代码。

/** object 表示单例 */
object EventBus {

    private val executorService: ExecutorService = Executors.newCachedThreadPool()
    private val subscriberMap = ConcurrentHashMap<EventType, CopyOnWriteArrayList<Subscription>>()
    private val methodHunter = SubscriberMethodHunter(subscriberMap)

    /** 注册订阅对象 */
    fun register(subscriber: Any) {
        executorService.execute {
            methodHunter.findSubscribeMethods(subscriber)
        }
    }

    /** 注销订阅对象 */
    fun unRegister(subscriber: Any) {
        executorService.execute {
            methodHunter.removeSubscribeMethods(subscriber)
        }
    }

    fun post(event: IEvent) {
        post(event, DEFAULT_TAG)
    }

    fun post(event: IEvent, tag: String) {
        val eventType = EventType(event.javaClass, tag)
        val list = methodHunter.getMatchEventType(eventType)
        list?.let {
            EventDispatcher.dispatcherEvent(event, it)
        }
    }

}
  • executorService: ExecutorService 是一个线程池,我们在注册和注销的时候,采用线程池,因为注册和注销是很频繁数量大的一个操作,�同时单个任务处理的时间比较短,使用线程池�可以很好的提高效率。
  • subscriberMap = ConcurrentHashMap<EventType, CopyOnWriteArrayList<Subscription>>() 这个就是我们存储�被观察者�与观察者的集合。
  • methodHunter = SubscriberMethodHunter(subscriberMap) 这个就是我们注册的核心类,下面会详细介绍。

注册 Register

/** 注册订阅对象 */
fun register(subscriber: Any) {
    executorService.execute {
        methodHunter.findSubscribeMethods(subscriber)
    }
}

subscriber: Any 是我们的观察者对象,它可以是一个 ActivityFragment�Service

举个例子来说注册的流程,当我们注册一个 ActivityEventBus 中时,我们通过 methodHunter.findSubscribeMethods(subscriber) 方法,查找出当前 Activity 中被@Subscriber关键字来标记方法,�判断的规则如下:

  1. @Subscriber 关键字标记
  2. �订阅函数只支持一个参数
  3. 订阅函数的参数是否实现了 IEvent 接口

当确定是我们要找的方法之后,根据方法的参数 IEventtag,�生成一个 EventType 实例作为当前方法的唯一key,根据观察者对象 subscriber: Any 和找到的事件执行方法 method 以及事件执行环境 mode,生成一个 Subscription

当然我们现在还不能直接把它存储到我们的集合中,回顾一下我们的集合结构,MutableMap<EventType, CopyOnWriteArrayList<Subscription>>,因为要先判断集合中是否已存在�一样的 key,当存在�则把当前 Subscription 添加到 CopyOnWriteArrayList<Subscription>> 中,这也是我们重写 EventTypeequals()hashCode() 的原因,�具体代码如下。

/** 查找对象中有注解标记的执行方法 */
@Synchronized fun findSubscribeMethods(subscriber: Any) {
    var clazz: Class<*>? = subscriber.javaClass
    while (clazz != null && !isSystemClass(clazz.name)) {
        val allMethods = clazz.declaredMethods
        for (method in allMethods) {
            val annotation = method.getAnnotation(Subscriber::class.java)
            if (annotation != null) {
                // 获取方法参数
                val paramsTypeClass = method.parameterTypes
                // 订阅函数只支持一个参数
                if (paramsTypeClass != null && paramsTypeClass.size == 1) {
                    val paramsEvent = paramsTypeClass[0]
                    if (isImplementIEvent(paramsEvent)) {
                        method.isAccessible = true
                        @Suppress("UNCHECKED_CAST")
                        val eventType = EventType(paramsEvent as Class<IEvent>, annotation.tag)
                        val subscription = Subscription(WeakReference(subscriber), method, annotation.mode)
                        subscribe(eventType, subscription)
                    }
                }
            }
        }

        clazz = clazz.superclass
    }
}

/** 根据 EventType 来确定一对多的订阅关系 */
@Synchronized private fun subscribe(type: EventType, subscription: Subscription) {
    var subscriptionLists: CopyOnWriteArrayList<Subscription>? = getMatchEventType(type)
    if (subscriptionLists == null) {
        subscriptionLists = CopyOnWriteArrayList()
    }

    if (subscriptionLists.contains(subscription)) {
        return
    }

    subscriptionLists.add(subscription)
    // 将事件类型key和订阅者信息存储到map中
    subscriberMap.put(type, subscriptionLists)
}

/** 判断是否有已存在的 EventType */
internal fun getMatchEventType(type: EventType): CopyOnWriteArrayList<Subscription>? {
    val keys = subscriberMap.keys
    return keys.firstOrNull { it == type }?.let { subscriberMap[it] }
}

发送事件 POST

�到这一步,我们已经把观察者对象注册到了 EventBus 中,剩下的就是在�需要的时候发送事件�就可以了,所以接下来,我们来看一下如何发送事件和执行事件�订阅的方法。

fun post(event: IEvent) {
        post(event, DEFAULT_TAG)
    }

fun post(event: IEvent, tag: String) {
    val eventType = EventType(event.javaClass, tag)
    val list = methodHunter.getMatchEventType(eventType)
    list?.let {
        EventDispatcher.dispatcherEvent(event, it)
    }
}

post 有两个方法,一个是使用默认的tag,一个是自己指定tag,这个方法很简单,我们根据发送事件时指定的 IEventtag,确定出 EventType,也就是我们集合中唯一的 key,通过 ke�y 得到 value,value 就是 CopyOnWriteArrayList<Subscription>,也就是当前事件执行方法的集合,剩下的就是执行调用这些方法。

事件分发 EventDispatcher

前面说了,我们在实现事件执行方法的时候,会�指定事件执行时的环境,MAINPOSTBACKGROUND,默认环境是 POST 线程,这样可以减少线程切换时带来的开销。

internal object EventDispatcher {

    /** POST 线程即事件发出的线程 */
    private val postHandler = PostEventHandler()
    /** MAIN 线程 UI 线程 */
    private val mainHandler = MainEventHandler()
    /** BACKGROUND 线程,ExecutorService 线程池 */
    private val asyncHandler = AsyncEventHandler()

    fun dispatcherEvent(event: IEvent, list: CopyOnWriteArrayList<Subscription>) {
        list.forEach {
            val eventHandler = getEventHandler(it.threadMode)
            eventHandler.handleEvent(it, event)
        }
    }

    private fun getEventHandler(mode: ThreadMode): EventHandler {
        return when (mode) {
            ThreadMode.POST -> postHandler
            ThreadMode.BACKGROUND -> asyncHandler
            ThreadMode.MAIN -> mainHandler
        }
    }

}

正如上面的代码,我们依据 threadMode ,选择�符合的 EventHandler,调用其 handleEvent(subscription: Subscription, event: IEvent) 去执行方法。

事件执行 EventHandler

internal interface EventHandler {
    fun handleEvent(subscription: Subscription, event: IEvent)
}

EventHandler 是一个接口,定义的事件执行时调用的方法 handleEvent(),�我们需要依次实现三种环境的具体实现。

  • PostEventHandler
internal class PostEventHandler: EventHandler {

    override fun handleEvent(subscription: Subscription, event: IEvent) {
        try {
            subscription.targetMethod.invoke(subscription.subscriber.get(), event)
        } catch (e: IllegalAccessException) {
            e.printStackTrace()
        } catch (e: IllegalArgumentException) {
            e.printStackTrace()
        } catch (e: InvocationTargetException) {
            throw e.targetException
        }
    }
}
  • MainEventHandler UI 线程
internal class MainEventHandler: EventHandler {

    private val handler = Handler(Looper.getMainLooper())

    override fun handleEvent(subscription: Subscription, event: IEvent) {
        handler.post({
            try {
                subscription.targetMethod.invoke(subscription.subscriber.get(), event)
            } catch (e: IllegalAccessException) {
                e.printStackTrace()
            } catch (e: IllegalArgumentException) {
                e.printStackTrace()
            } catch (e: InvocationTargetException) {
                throw e.targetException
            }
        })
    }

}
  • AsyncEventHandler 后台线程采用�线程池实现
internal class AsyncEventHandler: EventHandler {

    private val bgExecutor = Executors.newCachedThreadPool()

    override fun handleEvent(subscription: Subscription, event: IEvent) {
        bgExecutor.execute {
            try {
                subscription.targetMethod.invoke(subscription.subscriber.get(), event)
            } catch (e: IllegalAccessException) {
                e.printStackTrace()
            } catch (e: IllegalArgumentException) {
                e.printStackTrace()
            } catch (e: InvocationTargetException) {
                throw e.targetException
            }
        }
    }
}

�三种环境中,核心的执行方法都是一样的,采用反射来调用�具体事件的执行方法,仅仅是方法所在的执行环境不同而已。

到目前为止,我们的 EventBus 就完成了,通过注解的形式�实现事件的执行方法,�通过注册观察者对象,生成� key 和 value �建立关系存储到集合中,�在事件发出的时候,�查找出对应的事件方法集合,然后在指定的执行环境中调用。

有哪些写的不对的地方请提出,我会马上改正

我是 wanbo,一个会写代码的音乐人。

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

推荐阅读更多精彩内容