回调,观察者模式与总线

2020-09-17  本文已影响0人  chym

回调

在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和事件队列)以及粘性事件的实现上。

上一篇下一篇

猜你喜欢

热点阅读