带你手撸一个Kotlin版的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的原理图吧:

  • 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)

好了,万事俱备,只欠运行,开整:
在这里插入图片描述
可以发现基本功能咱们已经实现了,但是还有瑕疵,咱们接着往下看。

扩展

基本功能是实现了,但是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()
    }

然后来看一下运行结果:
在这里插入图片描述
可以看到应用直接崩溃了,奔溃原因很简单,因为咱们没做线程切换,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()
    }

再来运行看一下效果:
在这里插入图片描述

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

结尾

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

猜你喜欢

转载自blog.csdn.net/haojiagou/article/details/105363300