带你手撸一个Kotlin版的EventBus

简介: EventBus的优点有很多(现在来看也并不是优点):代码简洁,是一种发布订阅设计模式(观察者设计模式),简化了组件之间的通讯,分离了事件的发送者和接收者,而且可以随意切换线程,避免了复杂的和易错的依赖关系和生命周期问题

前言

EventBus在前两年用的人还是非常多的,它是由greenrobot 组织贡献的,该组织还贡献了GreenDao(目前不建议使用,建议使用官方的Room数据库框架)。EventBus的功能很简单,通过解耦发布者和订阅者简化Android事件传递,简单来说就是可以替代安卓传统的Intent、Handler、Broadcast或接口函数,在Activity、Fragment、Service之间进行数据传递。但是后来出现了RxBus(依赖于RxJava和RxAndroid),只通过短短几十行代码就撼动了EventBus江湖大哥的地位,可以好景不长,RxBus高兴了没几天官方又有了JetPack中的LiveData,也是几十行代码就能实现,而且无需依赖第三方包,还跟随生命周期,更加方便了。。。。但这些都不是本文的重点,虽然RxBus和LiveDataBus是目前首选,但EventBus统治了江湖这么久肯定有它的过人之处,所以本文就来手撸一个EventBus,来扒一扒江湖大哥EventBus。

EventBus介绍

这是EventBus的Github地址:https://github.com/greenrobot/EventBus

EventBus的优点有很多(现在来看也并不是优点):代码简洁,是一种发布订阅设计模式(观察者设计模式),简化了组件之间的通讯,分离了事件的发送者和接收者,而且可以随意切换线程,避免了复杂的和易错的依赖关系和生命周期问题。

大家应该都使用过EventBus,咱们使用的时候一般需要先写好注册和解注册,然后定义好接收方法,在接收方法上写好注解,在发送的地方通过Post方法将需要传递的对象传递出去,咱们刚才定义的接收方法就可以接收到传递的对象,并且咱们可以在接收方法上通过注解来修改线程。

说了这么多优点咱们来看一下EventBus的实现原理吧,先来看一下EventBus的原理图吧:

20200407140117534.png


  • EventBus底层采用的是注解和反射的方式来获取订阅方法信息(首先是注解获取,若注解获取不到,再用反射)
  • 当前订阅者是添加到Eventbus 总的事件订阅者的subscriptionByEventType集合中
  • 订阅者所有订阅的事件类型添加到typeBySubscriber 中,方便解注册时,移除事件

开始实现

说了这么多,该开始正文了。首先咱们模仿EventBus也来一个单例,Kotlin中实现单例非常简单,直接用object关键字修饰类,那么这个类就已经是最简单的懒汉式的单例了,当然也可以加双重检查锁等加锁算法,这里不做详解。来看一下代码吧:

object EventBus {}

简单吧,太简单了,接下来需要做的就是写上咱们需要的几个方法,想一下,平时咱们调用的时候一般只有三个方法:注册、解注册和发送方法,很明确,那就再来定义上这三个方法:

object EventBus {
    // 所有未解注册的缓存
    private val cacheMap: MutableMap<Any, List<SubscribeMethod>> = HashMap()
    /**
     * 注册
     * @param subscriber 注册的Activity
     */
    fun register(subscriber: Any) {}
    /**
     * 发送消息
     * @param obj 具体消息
     */
    fun post(obj: Any) {}
    /**
     * 取消注册
     * @param subscriber /
     */
    fun unRegister(subscriber: Any) { }
}

上面的代码还定义了一个Map,这里有一个小细节,我用的是MutableMap而不是Map,这是因为MutableMap是可变的map,而Map不可变(List在使用时也一样)。添加这个Map是为了保存注册了的类和类中的需要接收方法的集合,嗯,没错,SubscribeMethod就是订阅方法的类,下面就看一下SubscribeMethod的代码:

class SubscribeMethod(
    //注册方法
    var method: Method,
    //线程类型
    var threadModel: ThreadModel,
    //参数类型
    var eventType: Class<*>
)

上面代码就是Kotlin中实体类的写法(还可以为参数写默认值,加了默认值的参数在构造类时就可以不进行传递),这样就直接实现类get、set、toString方法,很简单吧?


接下来就该完善上面写的EventBus类中的方法了,先来思考一下注册方法,注册方法首先会去缓存中寻找是否存在,如果存在就证明已经注册,则不做操作,如果不存在那么就进行注册,嗯,看看代码吧:

/**
     * 注册
     * @param subscriber 注册的Activity
     */
    fun register(subscriber: Any) {
        var subscribeMethods = cacheMap[subscriber]
        // 如果已经注册,就不需要注册
        if (subscribeMethods == null) {
            subscribeMethods = getSubscribeList(subscriber);
            cacheMap[subscriber] = subscribeMethods;
        }
    }

大家可以看到上面代码中写了一个getSubscribeList方法,这个方法就是通过传进来的类来进行循环反射获取里面符合条件的方法(即有注解的接收方法),这里要注意,循环是因为有可能会将注册与解注册放在BaseActivity中,那么就需要循环便利子类和父类,通过分析,可以得出以下代码:

private fun getSubscribeList(subscriber: Any): List<SubscribeMethod> {
        val list: MutableList<SubscribeMethod> = arrayListOf()
        var aClass = subscriber.javaClass
        while (aClass != null) {
            aClass = aClass.superclass as Class<Any>
        }
        return list
    }

通过上面代码咱们已经获取到了所有的注册的类,但是这里需要将系统的类给过滤掉,系统的类肯定不可能注册EventBus啊,所以就有了下面代码:

//判断分类是在那个包下,(如果是系统的就不需要)
val name = aClass.name
if (name.startsWith("java.") ||
   name.startsWith("javax.") ||
   name.startsWith("android.") ||
   name.startsWith("androidx.")
) {
   break
}

过滤掉系统的类之后就需要判断方法了,通过反射获取到类中的所有方法,然后根据注解判断是否为咱们定义的接收方法,然后构造为咱们刚才定义的订阅方法类并放入List中:

val declaredMethods = aClass.declaredMethods
            declaredMethods.forEach {
                val annotation = it.getAnnotation(Subscribe::class.java) ?: return@forEach
                //检测是否合格
                val parameterTypes = it.parameterTypes
                if (parameterTypes.size != 1) {
                    throw RuntimeException("EventBus只能接收一个参数")
                }
                //符合要求
                val threadModel = annotation.threadModel
                val subscribeMethod = SubscribeMethod(
                    method = it,
                    threadModel = threadModel,
                    eventType = parameterTypes[0]
                )
                list.add(subscribeMethod)
            }

到这里获取类中的有注解咱们定义的方法就都获取到了,直接返回一个List给上面的注册方法,将List保存在缓存cacheMap中。

上面代码中进行判断是否有咱们定义的注解Subscribe,但是注解还没有定义,在Java中定义注解是在interfac关键字前添加@符号,在kotlin中则不然,代码如下:


@Target(AnnotationTarget.FUNCTION)
@Retention(value = AnnotationRetention.RUNTIME)
annotation class Subscribe(val threadModel: ThreadModel = ThreadModel.POSTING)

Target、Retention和Java中一样,都是作用域和执行时间。大家肯定注意到上面有一个ThreadModel咱们还没有定义,这个咱们放在后面再说(切换线程)。

注册方法就写完了,下面来写一下解注册,解注册很简单,如果Map中有对应的值,只需要将Map中对应的值remove掉即可,如果没有,则无需操作:

/**
     * 取消注册
     * @param subscriber /
     */
    fun unRegister(subscriber: Any) {
        val list = cacheMap[subscriber]
        //如果获取到
        if (list != null) {
            cacheMap.remove(subscriber)
        }
    }

接下来就该今天的核心代码了,通过Post方法将参数传递给接收方法并执行。思路很简单,直接在缓存中查找所有类,然后在循环中获取添加了注解的方法, 然后根据参数类型来判断方法是否应该接收事件:

/**
     * 发送消息
     * @param obj 具体消息
     */
    fun post(obj: Any) {
        val keys = cacheMap.keys
        val iterator = keys.iterator()
        while (iterator.hasNext()) {
            // 拿到注册类
            val next = iterator.next()
            //获取类中所有添加注解的方法
            val list = cacheMap[next]
            list?.forEach {
                //判断这个方法是否应该接收事件
                if (it.eventType.isAssignableFrom(obj::class.java)) {
                    //invoke需要执行的方法
                    invoke(it, next, obj)
                }
            }
        }
    }

上面代码在上面都分析过了,里面还有一个invoke方法需要来编写,这个方法很简单,就是来执行接收方法:

/**
     * 执行接收消息方法
     * @param it 需要接收消息的方法
     * @param next 注册类
     * @param obj 接收的参数(即post的参数)
     */
    private fun invoke(it: SubscribeMethod, next: Any, obj: Any) {
        val method = it.method
        method.invoke(next, obj)
    }

到这里基本的EventBus功能就已经实现了,咱们可以在下面进行测试。

测试

测试就来个简单的例子吧,只有两个Activity,先来看第一个,第一个里面只放一个TextView和一个Button,TextView用来显示一会传进来的值,Button用来跳转到第二个Activity,在Activity中进行注册与解注册,来看一下代码吧:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        EventBus.register(this)//注册
        initView()
    }
    private fun initView() {
        btnJump.setOnClickListener {
            startActivity(Intent(this, TwoActivity::class.java))
        }
    }
    override fun onDestroy() {
        super.onDestroy()//解注册
        EventBus.unRegister(this)
    }
}

还需要添加接收方法,别忘了添加注解:

@Subscribe
    fun zhu(person: Person) {
        tvText.text = "name=${person.name}   age=${person.age}"
    }

第一个Activity就写完了,下面来写第二个,第二个更简单,只有一个Button,用来Post一个对象:

class TwoActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_two)
        initView()
    }
    private fun initView() {
        btnSendMessage.setOnClickListener {
            EventBus.post(Person(name = "朱江",age = 23))
        }
    }
}

再来看一下Person类吧:

class Person(var name: String, var age: Int)

好了,万事俱备,只欠运行,开整:

20200407140034935.gif

可以发现基本功能咱们已经实现了,但是还有瑕疵,咱们接着往下看。

扩展

基本功能是实现了,但是EventBus还有一个非常重要的功能—切换线程,咱们可以在接收方法中进行指定线程来执行,咱们现在并没有实现。大家可以用上面的代码进行测试,测试方法很简单,直接把Post方法放在子线程中,然后在接收方法中弹一个吐司:

private fun initView() {
        btnSendMessage.setOnClickListener {
            Thread {
                EventBus.post(Person(name = "朱江",age = 23))
            }.start()
        }
    }
@Subscribe
    fun zhu(person: Person) {
        Toast.makeText(this,"name=${person.name}age=${person.age}",Toast.LENGTH_LONG).show()
    }

然后来看一下运行结果:

20200407140013790.gif

可以看到应用直接崩溃了,奔溃原因很简单,因为咱们没做线程切换,Post的时候放在了子线程,但接收方法中做了更新UI操作,所以肯定会崩溃。那么下面就来加一个线程切换吧。

上面代码中也有提到,ThreadModel类,上面注解中有提到,这是一个枚举类,里面定义了一些需要的线程,直接来看代码吧:

enum class ThreadModel {
    // 默认模式,无论post是在子线程或主线程,接收方法的线程为post时的线程。
    // 不进行线程切换
    POSTING,
    // 主线程模式,无论post是在子线程或主线程,接收方法的线程都切换为主线程。
    MAIN,
    // 主线程模式,无论post是在子线程或主线程,接收方法的线程都切换为主线程。
    // 这个在EventBus源码中与MAIN不同, 事件将一直排队等待交付。这确保了post调用是非阻塞的。
    // 此处不做其他处理,直接按照主线程模式处理
    MAIN_ORDERED,
    // 子线程模式,无论post是在子线程或主线程,接收方法的线程都切换为子线程。
    ASYNC
}

那么接下来咱们需要思考一下线程切换该怎么搞?其实线程切换只是接收方法存在的线程,咱们其实只需要更改Post方法中的invoke的执行就可以了啊,说来就来:

when (it.threadModel) {
        ThreadModel.POSTING -> {
            //默认情况,不进行线程切换,post方法是什么线程,接收方法就是什么线程
            EventBus.invoke(it, next, obj)
        }
        // 接收方法在主线程执行的情况
        ThreadModel.MAIN, ThreadModel.MAIN_ORDERED -> {
            // Post方法在主线程执行的情况
            if (Looper.myLooper() == Looper.getMainLooper()) {
                EventBus.invoke(it, next, obj)
            } else {
                // 在子线程中接收,主线程中接收消息
                EventBus.handler.post { EventBus.invoke(it, next, obj) }
            }
        }
        //接收方法在子线程的情况
        ThreadModel.ASYNC -> {
            //Post方法在主线程的情况
            if (Looper.myLooper() == Looper.getMainLooper()) {
                EventBus.executorService.execute(Runnable {
                    EventBus.invoke(
                        it,
                        next,
                        obj
                    )
                })
            } else {
                //Post方法在子线程的情况
                EventBus.invoke(it, next, obj)
            }
        }
    }

上面代码逻辑并不难,这里简单说一下吧,默认线程直接执行;主线程的话需要判断当前线程是否为主线程,如果是,直接执行,如果不是,通过Handler转为主线程再执行;子线程的话和主线程类似,不过需要将执行方法放入线程池,这样就是子线程中执行了。

接下来修改一下刚才的代码,在接收方法中添加上更换为主线程的注解:

@Subscribe(threadModel = ThreadModel.MAIN)
    fun zhu(person: Person) {
        //tvText.text = "name=${person.name}   age=${person.age}"
        Toast.makeText(this,"name=${person.name}   age=${person.age}",Toast.LENGTH_LONG).show()
    }

再来运行看一下效果:

20200407135922112.gif

可以发现已经成功了,咱们也实现了线程切换的功能,使用方法和EventBus一样,在注解中进行注明即可。

结尾

文章到这里基本结束了,上面的代码量其实并不多,大家可以进我的Github下载代码并运行,可以试着切换注解的线程试试,最后放一个本文所有代码的地址吧:https://github.com/zhujiang521/EventBus


目录
相关文章
|
5月前
|
JavaScript Java Android开发
kotlin安卓在Jetpack Compose 框架下跨组件通讯EventBus
**EventBus** 是一个Android事件总线库,简化组件间通信。要使用它,首先在Gradle中添加依赖`implementation &#39;org.greenrobot:eventbus:3.3.1&#39;`。然后,可选地定义事件类如`MessageEvent`。在活动或Fragment的`onCreate`中注册订阅者,在`onDestroy`中反注册。通过`@Subscribe`注解方法处理事件,如`onMessageEvent`。发送事件使用`EventBus.getDefault().post()`。
|
Java Android开发 C++
基于 Kotlin Coroutine 实现的 EventBus
基于 Kotlin Coroutine 实现的 EventBus
320 0
|
21天前
|
JSON 调度 数据库
Android面试之5个Kotlin深度面试题:协程、密封类和高阶函数
本文首发于公众号“AntDream”,欢迎微信搜索“AntDream”或扫描文章底部二维码关注,和我一起每天进步一点点。文章详细解析了Kotlin中的协程、扩展函数、高阶函数、密封类及`inline`和`reified`关键字在Android开发中的应用,帮助读者更好地理解和使用这些特性。
16 1
|
2月前
|
Android开发 开发者 Kotlin
告别AsyncTask:一招教你用Kotlin协程重构Android应用,流畅度飙升的秘密武器
【9月更文挑战第13天】随着Android应用复杂度的增加,有效管理异步任务成为关键。Kotlin协程提供了一种优雅的并发操作处理方式,使异步编程更简单直观。本文通过具体示例介绍如何使用Kotlin协程优化Android应用性能,包括网络数据加载和UI更新。首先需在`build.gradle`中添加coroutines依赖。接着,通过定义挂起函数执行网络请求,并在`ViewModel`中使用`viewModelScope`启动协程,结合`Dispatchers.Main`更新UI,避免内存泄漏。使用协程不仅简化代码,还提升了程序健壮性。
70 1
|
3月前
|
调度 Android开发 开发者
【颠覆传统!】Kotlin协程魔法:解锁Android应用极速体验,带你领略多线程优化的无限魅力!
【8月更文挑战第12天】多线程对现代Android应用至关重要,能显著提升性能与体验。本文探讨Kotlin中的高效多线程实践。首先,理解主线程(UI线程)的角色,避免阻塞它。Kotlin协程作为轻量级线程,简化异步编程。示例展示了如何使用`kotlinx.coroutines`库创建协程,执行后台任务而不影响UI。此外,通过协程与Retrofit结合,实现了网络数据的异步加载,并安全地更新UI。协程不仅提高代码可读性,还能确保程序高效运行,不阻塞主线程,是构建高性能Android应用的关键。
57 4
|
4月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
**Kotlin中的`by lazy`和`lateinit`都是延迟初始化技术。`by lazy`用于只读属性,线程安全,首次访问时初始化;`lateinit`用于可变属性,需手动初始化,非线程安全。`by lazy`支持线程安全模式选择,而`lateinit`适用于构造函数后初始化。选择依赖于属性特性和使用场景。**
140 5
Android经典面试题之Kotlin延迟初始化的by lazy和lateinit有什么区别?
|
4月前
|
安全 Android开发 Kotlin
Android经典面试题之Kotlin中常见作用域函数
**Kotlin作用域函数概览**: `let`, `run`, `with`, `apply`, `also`. `let`安全调用并返回结果; `run`在上下文中执行代码并返回结果; `with`执行代码块,返回结果; `apply`配置对象后返回自身; `also`附加操作后返回自身
57 8
|
4月前
|
安全 Java Android开发
探索Android应用开发中的Kotlin语言
【7月更文挑战第19天】在移动应用开发的浩瀚宇宙中,Kotlin这颗新星以其简洁、安全与现代化的特性,正迅速在Android开发者之间获得青睐。从基本的语法结构到高级的编程技巧,本文将引导读者穿梭于Kotlin的世界,揭示其如何优化Android应用的开发流程并提升代码的可读性与维护性。我们将一起探究Kotlin的核心概念,包括它的数据类型、类和接口、可见性修饰符以及高阶函数等特性,并了解这些特性是如何在实际项目中得以应用的。无论你是刚入门的新手还是寻求进阶的开发者,这篇文章都将为你提供有价值的见解和实践指导。
|
4月前
|
SQL 安全 Java
Android经典面试题之Kotlin中object关键字实现的是什么类型的单例模式?原理是什么?怎么实现双重检验锁单例模式?
Kotlin 单例模式概览 在 Kotlin 中,`object` 关键字轻松实现单例,提供线程安全的“饿汉式”单例。例如: 要延迟初始化,可使用 `companion object` 和 `lazy` 委托: 对于参数化的线程安全单例,结合 `@Volatile` 和 `synchronized`
55 6
|
4月前
|
存储 前端开发 测试技术
Android Kotlin中使用 LiveData、ViewModel快速实现MVVM模式
使用Kotlin实现MVVM模式是Android开发的现代实践。该模式分离UI和业务逻辑,借助LiveData、ViewModel和DataBinding增强代码可维护性。步骤包括创建Model层处理数据,ViewModel层作为数据桥梁,以及View层展示UI。添加相关依赖后,Model类存储数据,ViewModel类通过LiveData管理变化,而View层使用DataBinding实时更新UI。这种架构提升代码可测试性和模块化。
173 2