美文网首页
回调,观察者模式与总线

回调,观察者模式与总线

作者: chym | 来源:发表于2020-09-17 10:12 被阅读0次

    回调

    在Android开发中,回调无处不在,我们用它进行类与类的通信,并组成其他设计模式。Android系统API中也提供给了我们大量回调函数,用于类的定制,生命周期的监听,用户输入事件的通知等。对于这些系统回调,如果是单函数接口(SAM),我们还会写成lambda表达式的形式。

    ViewPager有页面滚动回调设置函数void setOnPageChangeListener(OnPageChangeListener listener),内部使用一个变量存储该回调。不过该方法已经被标记为了@Deprecated。因为需要满足不止一个地方需要接受回调的情景,ViewPager储存回调接口的变量类型由OnPageChangeListener变为了List<OnPageChangeListener>,回调的设置方法也变为了void addOnPageChangeListener(OnPageChangeListener listener)。通常这种单一事件的监听,仅仅把它称为回调,可当回调接口被按List的形成组织起来时,我们就想起了观察者模式。

    观察者模式

    观察者模式也叫发布订阅模式,这两个名字很好的体现了该模式的关键组成要素:观察者与被观察者两个类,以及被观察者到观察者的发布和观察者向被观察者的订阅两个动作。Java API中的Observable类和Observer接口实现了标准的观察者模式,只是因为Java的单继承的限制与观察者模式足够简单,通常我们并不会使用该API而是选择自己实现观察者模式。

    回到前面提到的接口,能够发现:ViewPager对应被观察者,OnPageChangeListener对应观察者,void addOnPageChangeListener(OnPageChangeListener listener)对应订阅,void removeOnPageChangeListener (OnPageChangeListener listener)对应取消订阅,回调接口中回调函数的调用对应发布。观察者模式中的要素在事件回调中完全得到了体现,这种简单的事件回调称为观察者模式其实也未尝不可。只是观察者模式的发布方法通常由我们自己调用,而事件回调函数的调用通常由系统触发。下面再进一步,考虑总线与观察者模式的关系,二者是也是及其接近的。

    总线

    通常我们理解的总线能接收各种类型的信息,这些信息又被需要的地方获取,达到通信与解耦的目的。我们按照这个描述来实现一个最简单的总线。

    object Bus {
        private val subscriberList = mutableListOf<Subscriber>()
    
        fun register(subscriber: Subscriber) {
            subscriberList.add(subscriber)
        }
    
        fun unregister(subscriber: Subscriber) {
            subscriberList.remove(subscriber)
        }
    
        fun post(message: Any) {
            subscriberList.forEach {
                it.onMessage(message)
            }
        }
    }
    
    interface Subscriber {
        fun onMessage(message: Any)
    }
    

    总线与观察者模式对比,能发现几点不同:

    1. 总线中被观察者不见了,通信的各方是对等的,都能注册为Subscriber接收消息,或直接发布消息;
    2. 发布的过程由各式回调的方式变为了传递message,强调的点从执行动作变为了信息传递。

    这个破产版的总线虽然简陋,但涵盖了事件总线最基本的要点。对比一下Android中使用的EventBus,有如下不同或缺少相应能力:

    1. 线程安全:Bus注册与注销不是线程安全的;
    2. 消息类型:Bus的消息传递与接收类型为Any,虽然可以传递任何类型,但消息的接收处必需进行类型的判断来确定消息是自己需要的;
    3. 消息接收:Bus解收总线消息必须要实现Subscriber,且消息接收方法必须是onMessage
    4. 线程切换:Bus接收消息时不能进行线程切换;
    5. 粘性事件:Bus不支持粘性事件,消息发布后就不能被新注册的Subscriber接收到了。

    下面我们来丰富Bus的功能。

    进一步完善的总线

    不到50行代码,就能实现一个较为完善的事件总线了:

    object Bus {
        private val methodMap = mutableMapOf<Class<*>, MutableList<SubscribeMethod>>()
        private val mainHandler = Handler(Looper.getMainLooper())
    
        @Synchronized
        fun register(subscriber: Any) {
            subscriber.javaClass.declaredMethods.filter {
                it.isAnnotationPresent(Sub::class.java) && it.parameterTypes.size == 1
            }.forEach { method ->
                val key = method.parameterTypes[0]
                val methodList = methodMap[key] ?: mutableListOf<SubscribeMethod>().apply {
                    methodMap[key] = this
                }
                methodList.add(SubscribeMethod(subscriber, method))
            }
        }
    
        @Synchronized
        fun unregister(subscriber: Any) {
            subscriber.javaClass.declaredMethods.filter {
                it.isAnnotationPresent(Sub::class.java) && it.parameterTypes.size == 1
            }.forEach { method ->
                val key = method.parameterTypes[0]
                methodMap[key]?.removeAll { subscribeMethod ->
                    subscribeMethod.method == method
                }
            }
        }
    
        fun post(message: Any) {
            methodMap[message.javaClass]?.forEach {
                if (it.method.getAnnotation(Sub::class.java)?.thread == Thread.Main) {
                    mainHandler.post { it.method.invoke(it.subscriber, message) }
                } else {
                    it.method.invoke(it.subscriber, message)
                }
            }
        }
    
        class SubscribeMethod(val subscriber: Any, val method: Method)
    }
    
    @kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
    @kotlin.annotation.Target(AnnotationTarget.FUNCTION)
    annotation class Sub(val thread: Thread = Thread.Original)
    
    enum class Thread {
        Main,
        Original
    }
    

    对比上面指出的5种破产版总线的相较EventBus缺失的能力,新版总线完善了前4种。

    第1种能力通过设置注册与注销为同步方法,保证其线程安全。

    第2, 3种能力通过修改对观察者的不同存储方式实现。将Subscriber的列表升级为Map,Map的每一项中,key为消息类型,value为订阅该消息类型的所有方法。这样就能在总线内部区分消息类型,进行不同的消息通知,而不需要在接收消息处进行消息类型判断了。注册时,查找到订阅者的所有被Sub注解的单参数方法,认为其是消息接收方法,将参数类型作为消息(key)类型,把这些方法归类到Map中。注销与注册相反,将这些方法从Map中移除。发布消息时,根据消息类型取出所有方法调用即可。需要注意的是方法需要与订阅者一起保存,这样才能调用该方法。

    第4种能力通过为注解增加参数,获取是否需要切换到主线程,来决定是否用Handler切换线程即可。

    至此,我们实现了相较EventBus缺失的前4种能力。对照EventBus源码,其功能相较我们的Bus主要完善在了优化Bus中的订阅注销过程的遍历(通过多个Map),更多的订阅消息线程指定(通过ThredLocal和事件队列)以及粘性事件的实现上。

    相关文章

      网友评论

          本文标题:回调,观察者模式与总线

          本文链接:https://www.haomeiwen.com/subject/riflyktx.html