回调,观察者模式与总线

回调

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容