换个姿势,更好地参透EventBus(中)

简介: EventBus(事件总线),跟之前写的 Handler 一样,老生常谈,教程早已烂大街

0x5、见招拆招 → 手写一个更轻量级的广播


① 观察者模式常规写法 → 先写个雏形


不了解观察者模式的可以看:把书读薄 | 《设计模式之美》设计模式与范式(行为型-观察者模式),直接开敲:


// 传递数据类
data class Entity(
    val key: String,
    var value: Any
)
// 更新回调接口
interface IUpdate {
    fun updateData(entity: Entity)
}
// 观察者抽象类
abstract class Observer: IUpdate
// 被观察者
object Subject {
    private val observerList = arrayListOf<Observer>()
    fun register(observer: Observer) {
        this.observerList.add(observer)
    }
    fun unregister(observer: Observer) {
        this.observerList.remove(observer)
    }
    fun postMessage(entity: Entity) {
        observerList.forEach { it.updateData(entity) }
    }
}


简单如斯,接着可以写个简单的实例验证下,A → B → C → D,D发送消息,ABC接收消息(页面一样~):


class ATestActivity : AppCompatActivity() {
    // 观察者回调
    val mObserver: Observer = object : Observer() {
        override fun updateData(entity: Entity) {
            tv_content.text = entity.value.toString()
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_index)
        tv_title.text = "页面A"
        bt_test.setOnClickListener {
            startActivity(Intent(this, BTestActivity::class.java))
        }
        // 注册事件
        Subject.register(mObserver)
    }
    override fun onDestroy() {
        super.onDestroy()
        // 取消事件注册
        Subject.unregister(mObserver)
    }
}
class DTestActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_index)
        bt_test.setOnClickListener {
            // 发送事件
            Subject.postMessage(Entity("back_data", "页面D的返回数据~"))
            finish()
        }
    }
}


运行效果如下


网络异常,图片无法展示
|


D发送了广播,然后ABC都收到,且进行了页面更新,可以,雏形有了,开始着手优化。


② 只关注想关注的广播 →  数据结构优化


被观察者Subject的postMessage()直接对列表里的所有观察者进行了遍历,有点过于粗暴了,毕竟  观察者不一定要关注被观察者的所有行为,以这个为切入点,引入key,同时优化下存储的数据结构:


object Subject {
    private val observerMap = hashMapOf<String, ArrayList<Observer>>()
    fun register(key: String, observer: Observer) {
        val observerList = observerMap[key]
        if (observerList.isNullOrEmpty()) {
            observerMap[key] = arrayListOf()
        }
        observerMap[key]!!.add(observer)
    }
    fun unregister(key: String, observer: Observer) {
        if (observerMap[key].isNullOrEmpty()) return
        observerMap[key]!!.remove(observer)
    }
    fun postMessage(key: String, entity: Entity) {
        if (observerMap[key].isNullOrEmpty()) return
        observerMap[key]!!.forEach { it.updateData(entity) }
    }
}


通过不同的key对订阅者进行区分,减少了无效遍历,但是也带来了一个问题,注册、解注册、发送广播都要传多一个key。


Subject.register("back_data", mObserver)
Subject.unregister("back_data", mObserver)
Subject.postMessage("back_data", Entity("back_data", "页面D的返回数据~"))


嗯,传两个参数看着不是很优雅,发送广播可以把Key整到Entity里,注册和解注册可以搞到Observer类中,改动代码如下:


// 抽象观察者
abstract class Observer: IUpdate {
    abstract val key: String
}
// 观察者回调
val mObserver: Observer = object : Observer() {
    override val key = "back_data"
    override fun updateData(entity: Entity) {
        tv_content.text = entity.value.toString()
    }
}
// 被观察者
object Subject {
    private val observerMap = hashMapOf<String, ArrayList<Observer>>()
    fun register(observer: Observer) {
        val observerList = observerMap[observer.key]
        if (observerList.isNullOrEmpty()) {
            observerMap[observer.key] = arrayListOf()
        }
        observerMap[observer.key]!!.add(observer)
    }
    fun unregister(observer: Observer) {
        if (observerMap[observer.key].isNullOrEmpty()) return
        observerMap[observer.key]!!.remove(observer)
    }
    fun postMessage(entity: Entity) {
        if (observerMap[entity.key].isNullOrEmpty()) return
        observerMap[entity.key]!!.forEach { it.updateData(entity) }
    }
}


③ FBI WARNING:警惕内存泄漏的风险


上面的代码,看上去好像没啥问题,是吧?但...真的没问题吗?


上面我们偷懒用的匿名内部类,它存在这样的问题:


匿名内部类会持有外部类的引用,此处的外部类是Activity,如果忘记解绑(移除集合),会导致onDestory()后,Subject中的集合依旧持有Activity引用。当Subject遍历执行到此回调时,BOOM!内存泄漏就来了~~


验证方法很简单,build.gradle依赖下LeakCanary:


debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'


然后页面故意漏掉某个Observer的解绑,然后此页面finish()掉后,在另一个页面发起一个事件,尝试几次后会发现:


网络异常,图片无法展示
|


所以,切记解绑 !!!一种比较无脑的解绑方式(笨,但是稳健~):


在页面基类里定义一个集合,把每个Observer实例都加入其中,在onDestory()中遍历取消注册


示例如下


protected val mObserverList = arrayListOf<Observer>()
// 直接将观察者加入列表
mObserverList.add(object : Observer() {
    override val key = "other_data"
    override fun updateData(entity: Entity) {
        tv_content.text = entity.value.toString()
    }
})
// 遍历列表注册
mObserverList.forEach { Subject.register(it) }
override fun onDestroy() {
    super.onDestroy()
    // 遍历取消事件注册
    mObserverList.forEach { Subject.unregister(it) }
}


④ 谁是卧底 → 谁才是真正的观察者


知道要规避内存泄漏风险后,继续优化,在使用过程中不难发现这样的问题:


一个观察者可能对观察者的多种行为进行观察,行为有多少种,就要实例化多少个Observer


em...好像有点不对劲,这TM是把行为作为了观察者啊,观察者应该包裹各种行为的回调,明显 页面才是观察者,简单,页面直接实现IUpdate接口,重写更新数据的方法。


class ATestActivity : AppCompatActivity(), IUpdate {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_index)
        tv_title.text = "页面A"
        bt_test.setOnClickListener {
            startActivity(Intent(this, BTestActivity::class.java))
        }
        Subject.register(this)
    }
    override fun updateData(entity: Entity) {
        when(entity.key) {
            "back_data" -> tv_content.text = entity.value.toString()
        }
    }
    override fun onDestroy() {
        super.onDestroy()
        Subject.unregister(this)
    }
}
// 变回原样的被观察者
object Subject {
    private val observerList = arrayListOf<IUpdate>()
    fun register(observer: IUpdate) {
        observerList.add(observer)
    }
    fun unregister(observer: IUpdate) {
        observerList.remove(observer)
    }
    fun postMessage(entity: Entity) {
        observerList.forEach { it.updateData(entity) }
    }
}


可以是可以,但postMessage()又变回之前的无脑遍历状态了,因为页面传这个Key有点麻烦,毕竟实现的IUpdate接口。如果在页面中定义额外的key属性,在Subject里拿到观察者还得做下类型强转拿Key。


简单点说:被观察者还得知道观察者具体的类型,这又耦合了...


这个问题先放一放,等下会解决,这里思考另一个问题:


既然暂时没法有脑遍历了,那广播Entity里的Key还有必要吗?


没有,直接定义不同的广播类型,观察者直接判断类型执行对应操作就好了,改动后的代码:


interface IUpdate {
    fun updateData(any: Any)
}
object Subject {
    private val observerList = arrayListOf<IUpdate>()
    fun register(observer: IUpdate) {
        observerList.add(observer)
    }
    fun unregister(observer: IUpdate) {
        observerList.remove(observer)
    }
    fun postMessage(entity: Any) {
        observerList.forEach { it.updateData(entity) }
    }
}
// 传递数据
data class DataEntity(var data: String)
// 刷新页面
object RefreshEntity
// 测试页面
class ATestActivity : AppCompatActivity(), IUpdate {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_index)
        tv_title.text = "页面A"
        bt_test.setOnClickListener {
            startActivity(Intent(this, BTestActivity::class.java))
        }
        Subject.register(this)
    }
    // 对应不同的广播执行不同的处理
    override fun updateData(any: Any) {
        when (any) {
            is DataEntity -> tv_content.text = any.data
            is RefreshEntity -> Toast.makeText(this, "收到更新广播", Toast.LENGTH_SHORT).show()
        }
    }
    override fun onDestroy() {
        super.onDestroy()
        Subject.unregister(this)
    }
}
class BTestActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_index)
        tv_title.text = "页面B"
        bt_test.setOnClickListener {
            Subject.postMessage(DataEntity("回传数据"))
            Subject.postMessage(RefreshEntity)
            finish()
        }
    }
}


看到这里,有用过EventBus的童鞋,肯定会哔一句:这TM不就是EventBus吗


像,但还不是,你用EventBus,Activity和Fragment需要实现接口吗?所以接下来想办法让观察者不用实现接口。


⑤ 巧用反射 → 少写一个接口


假设我们约定成俗,只要类中定义了 updateData(any: Any) 方法,我们就把它当做观察者的回调方法。


所以要做的就是获取观察者类所有的方法,遍历匹配 方法名和参数个数,符合的方法就是回调方法,用 反射 实现一波。修改后的被观察者:


object Subject {
    private val observerMap = hashMapOf<Any, Method?>()
    fun register(any: Any) {
        var method: Method? = null
        try {
            // 反射获得此方法
            method = any.javaClass.getDeclaredMethod("updateData", Any::class.java)
            observerMap[any] = method
        } catch (e: NoSuchMethodException) {
            e.printStackTrace()
        }
    }
    fun unregister(any: Any) {
        observerMap[any] = null
        observerMap.remove(any)
    }
    fun postMessage(entity: Any) {
        observerMap.forEach { (key, value) ->
            value?.invoke(key, entity)
        }
    }
}


把观察者类:实现IUpdate接口和updateData(any: Any)前override标识干掉,运行验证一波,效果一致。


⑥ 巧用运行时注解 → 规避方法名写错


上面通过反射省去了实现一个接口,但也带来了隐患,方法不能自动生成,只能手敲或复制粘贴,就有可能出现函数名拼写错误的问题,毕竟人是容易犯错的


能不能显式地告诉编译器:这函数就是观察者的回调方法,你不用理他叫啥


还真可以,利用 注解 就可以实现,有些朋友可能对注解有些陌生(没用过),没关系,简单过一波~


Java里都见过 @Override 吧,在重写方法时要加上此注解,否则编译器直接爆红,无法编译。这里的@Override就是注解,用于告知编译器正在重写一个方法,这样,当父类方法被删除或修改时,编译器会提示错误信息。


注解可用来修饰类、方法、参数等,有下述三种使用场景:


  • 编译器提示信息:给编译器用来发现错误,或清除不必要的警告;
  • 编译时生成代码:利用编译器外的工具(如kapt)根据注解信息自动生成代码;
  • 运行时处理:在运行时根据注解,通过反射获得具体信息,然后做一些操作;


限于篇幅就不讲解注解相关的姿势了,可自行搜索资料学习,或参见 《Kotlin实用教程 | 0x9 - 注解与反射》,Java 和 Kotlin 的注解规则有点不一样哈~


此处应用注解正是第三种场景,先定义一个注解类:


// 与Java用 @interface 声明注解不同,kotlin使用 annotation class 进行声明
@Target(AnnotationTarget.FUNCTION)  // 修饰函数
@Retention(AnnotationRetention.RUNTIME) // 函数的保留存活时间
annotation class Subscribe


接着获取订阅者类所有的方法,判断修饰符及是否包含Subscribe注解,然后加入集合:


// 修改后的被观察者
object Subject {
    private const val BRIDGE = 0x40
    private const val SYNTHETIC = 0x1000
    private const val MODIFIERS_IGNORE = Modifier.ABSTRACT or Modifier.STATIC or BRIDGE or SYNTHETIC
    private val observerMap = hashMapOf<Any, Method?>()
    fun register(any: Any) {
        try {
            val methods = any.javaClass.declaredMethods
            methods.forEach {
                val modifiers = it.modifiers
                // 判断方法修饰符是否为public,不是Static、ABSTRACT等修饰符
                if (modifiers and Modifier.PUBLIC != 0 && modifiers and MODIFIERS_IGNORE == 0) {
                    // 获取参数列表
                    val parameterTypes = it.parameterTypes
                    // 判断参数是否只有一个
                    if (parameterTypes.size == 1) {
                        // 获取SubScribe注解
                        val subscribeAnnotation = it?.getAnnotation(Subscribe::class.java)
                        // Subscribe注解不为空的,把回调方法加入集合
                        subscribeAnnotation?.let { _ -> observerMap[any] = it }
                    }
                }
            }
        } catch (e: NoSuchMethodException) {
            e.printStackTrace()
        }
    }
    fun unregister(any: Any) {
        observerMap[any] = null
        observerMap.remove(any)
    }
    fun postMessage(any: Any) {
        observerMap.forEach { (key, value) ->
            value?.invoke(key, any)
        }
    }
}
// 为观察者回调方法添加@Subscribe注解,并修改为别的函数名
@Subscribe
fun onMainEvent(any: Any) {
    when (any) {
        is DataEntity -> tv_content.text = any.data
        is RefreshEntity -> Toast.makeText(this, "收到更新广播", Toast.LENGTH_SHORT).show()
    }
}


运行效果一致,而且又想到了一个好玩的东西,还记得上面②那里写的:观察者不一定要关注被观察者的所有行为,利用注解,我们可以对不同的 广播类型 进行区分,观察者按需关注对应广播~


相关文章
|
3月前
|
存储 Web App开发 运维
发布、部署,傻傻分不清楚?从概念到实际场景,再到工具应用,一篇文章让你彻底搞清楚
部署和发布是软件工程中经常互换使用的两个术语,甚至感觉是等价的。然而,它们是不同的! • 部署是将软件从一个受控环境转移到另一个受控环境,它的目的是将软件从开发状态转化为生产状态,使得软件可以为用户提供服务。 • 发布是将软件推向用户的过程,应用程序需要多次更新、安全补丁和代码更改,跨平台和环境部署需要对版本进行适当的管理,有一定的计划性和管控因素。
172 1
|
12月前
|
人工智能 小程序
行动派:想到就做,无关乎与成功或失败,重在过程!
行动派:想到就做,无关乎与成功或失败,重在过程!
154 0
|
12月前
|
算法 Java
手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏17敌人自动追踪(自动寻路)
手把手一步一步教你使用Java开发一个大型街机动作闯关类游戏17敌人自动追踪(自动寻路)
111 0
|
设计模式 SQL Java
有点狠有点猛,我用责任链模式重构了业务代码
文章开篇,抛出一个老生常谈的问题,学习设计模式有什么作用? 设计模式主要是为了应对代码的复杂性,让其满足开闭原则,提高代码的扩展性 另外,学习的设计模式 一定要在业务代码中落实,只有理论没有真正实施,是无法真正掌握并且灵活运用设计模式的 这篇文章主要说 责任链设计模式,认识此模式是在读 Mybatis 源码时, Interceptor 拦截器主要使用的就是责任链,当时读过后就留下了很深的印象(内心 OS:还能这样玩)
|
XML 架构师 Java
一文把Java反射说的明明白白,清清楚楚,记得点赞关注,距离架构师的小目标又进一步
今天有时间没加班回家来好好写一篇文章,反射是Java里比较高级的概念了,一般在书的后半部分。反射也是写框架的必备技能,反射很重要,现在仍然记得刚毕业的一两年一直没有搞懂反射是什么。今天就讲讲反射,希望这篇文章能帮有同样疑惑的你解开疑团,废话不多说,让我们开始吧。
156 0
一文把Java反射说的明明白白,清清楚楚,记得点赞关注,距离架构师的小目标又进一步
|
设计模式 缓存 Android开发
换个姿势,更好地参透EventBus(上)
EventBus(事件总线),跟之前写的 Handler 一样,老生常谈,教程早已烂大街
215 1
|
安全 Java API
换个姿势,更好地参透EventBus(下)
EventBus(事件总线),跟之前写的 Handler 一样,老生常谈,教程早已烂大街
105 0
|
Java Spring
9条消除if...else的锦囊妙计,助你写出更优雅的代码(下)
9条消除if...else的锦囊妙计,助你写出更优雅的代码(下)
|
程序员 Android开发
牛逼!终于有人能把Android事件分发机制讲明白了
在Android开发中,事件分发机制是一块Android比较重要的知识体系,了解并熟悉整套的分发机制有助于更好的分析各种点击滑动失效问题,更好去扩展控件的事件功能和开发自定义控件,同时事件分发机制也是Android面试必问考点之一,如果你能把下面的一些事件分发图当场画出来肯定加分不少。废话不多说,总结一句:事件分发机制很重要。
牛逼!终于有人能把Android事件分发机制讲明白了
|
设计模式 算法 Java
9条消除if...else的锦囊妙计,助你写出更优雅的代码
9条消除if...else的锦囊妙计,助你写出更优雅的代码