回调,观察者模式与总线
回调
在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)
}
总线与观察者模式对比,能发现几点不同:
- 总线中被观察者不见了,通信的各方是对等的,都能注册为
Subscriber
接收消息,或直接发布消息; - 发布的过程由各式回调的方式变为了传递message,强调的点从执行动作变为了信息传递。
这个破产版的总线虽然简陋,但涵盖了事件总线最基本的要点。对比一下Android中使用的EventBus,有如下不同或缺少相应能力:
- 线程安全:
Bus
注册与注销不是线程安全的; - 消息类型:
Bus
的消息传递与接收类型为Any
,虽然可以传递任何类型,但消息的接收处必需进行类型的判断来确定消息是自己需要的; - 消息接收:
Bus
解收总线消息必须要实现Subscriber
,且消息接收方法必须是onMessage
; - 线程切换:
Bus
接收消息时不能进行线程切换; - 粘性事件:
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和事件队列)以及粘性事件的实现上。