轻量级事件总线框架 - SwiftyEventBus

LOGO.png

前言

由于最近划水的厉害,CodeReview竟然不知道拿什么代码比较好,于是乎翻起了一年多以前写的框架SwiftyEventBus来向全部门的同事谢罪。

在iOS开发中NSNotificationCenter一定是作为入门级别所要掌握的框架,然而当我们尝试过其他技术平台类似的框架,或者说对事件通知机制进行一定的思考之后,我们发现NSNotificationCenter这个陈年老框架实在是不能符合新时代的标准。更何况,Swift发布已然许久,Swift如此强大的类型系统NSNotificationCenter竟然不能受益半分,实在是让人捶胸顿足,仰天长叹。

于是乎,在偷窥了隔壁安卓的EventBus以及结合Swift本身的特性之后,尝试构建了一套属于Swift自己的EventBus,也就是上述所提到的SwiftyEventBus。当然,在开始着手构建框架之前我也在github上搜了一下类似的框架,发现了一款SwiftEventBus,这个项目的star有800之多,我的眼神瞬间暗淡了下来,“原来我的想法已经被人实现了”,然而在我翻看完源码之后发现事情并没有那么简单,原来这个项目仅仅只是对NSNotificationCenter的简单100多行的Wapper!世上安能有如此“欺名盗世”之徒?!于是更加坚定了我自己做框架的决心,并且最后运行在自己公司的项目上。

再见,NSNotificationCenter

EventBus的核心概念就是事件的接收以及分发,常用的场景是在App中跨组件的通信以及一对多的通知等。然而核心的概念虽然简单,但是设计到的框架设计还是要考虑方方面面,比如事情如何接收,如何区分不同的事件,不同线程下事件的处理,事件的优先级等等。在此之前我们先来看看NSNotificationCenter存在最主要的问题?

1. 事件区分

在NSNotificationCenter中,事件的区分是通过NotificationName来进行区分的,也就是说只有NotificationName相同才能在观察所注册到的地方进行方法调用,也就说我们每次进行NSNotificationCenter进行分发事件的时候都需要取一个符合规则的且不重复的名字。这里存在两个问题:一,命名永远是开发中最难以抉择的事情,不命名或者少命名可以明显提高开发的幸福感,更不用说苹果所推崇的NotificationName的命名规则,诸如“com.apple.myapp.home.finish”之类的命名实在是冗长,而且对于新手来说极其容易造成硬编码的散落,后期维护困难。二,所谓的不重复的名字也只是大多数情况的不重复,如果一旦使用不当选取了重复的事件名,那么就会造成非常难以排查的bug。

2. 事件分发

我们都知道,NSNotificationCenter的事件派发机制使用的是iOS开发中非常传统的target-action的方法,这在传统iOS开发中也是非常合乎常理的方式,然而在Swift发布之后这一切似乎不是那么合理。因为Swift并不是只能用于开发iOS,理论上我们也可以在Linux上运行我们的Swift项目,然而在这样的环境之下是没有OC的运行时,也就是说没有target-action的,换句话说这样的事件分发在这个情况下是缺失的。

3. 类型安全

在OC中我们对这样的代码会觉得非常的平常:

- (void)viewDidLoad
{
    [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNotification:) name:MyName object:nil];
    [[NSNotificationCenter defaultCenter] postNotificationName:MyName object:nil userInfo:@{@"foo": @"foo-value"}];
}

- (void)handleNotification:(NSNotification *)notification {
    NSString *foo = [notification userInfo][@"foo"];
    NSLog(@"%@", foo);
}

然而这样的代码却存在潜在的类型安全隐患,最主要的根源是,NSNotificationCenter是通过NSDictionary来传递信息的,而NSDictionary其实是NSDictionary<NSString, id>类型,如果发送方传递的不是字符串类型而是其他的类型,而接收方依旧按照字符串的类型来处理,那么很有可能程序就会发生crash。

4. 线程切换

NSNotificationCenter在设计上并没有对线程切换做任何的考虑,但是在现实世界中这样的使用场景非常常见,比如在某个子线程中发送通知,然后在主线程更新UI。我们都知道,NSNotificationCenter的收发通知都是在同一个线程的,也就是说对于新手来说,很容易发生在子线程发送通知,但是忘记了在处理通知的时候将操作切换为主队列,从而造成相关的问题。

你好,SwiftyEventBus

SwiftyEventBus旨在解决上述的问题,并且引入了对于事件分发的高级操作,以下是SwiftyEventBus最简单的一个例子:

class DemoViewController: UIViewController {
    var ob: EventSubscription<String>!
    override func viewDidLoad() {
        super.viewDidLoad()
        ob = EventBus.`default`.register { (x: String) in
            print(x)
        }
    }
}

// anywhere
EventBus.default.post("Foo")

1. 事件区分

首先从使用上来说更加方便使用了,我们不需要再为通知取名而费尽心思,只需要简单的registerpost就可以完成整套的操作,当然在现实世界中不建议直接传递基本数据类型,这样会损坏通知的表达能力,建议可以这么做:

struct DoSomeThingMessage {
    let foo: String
}

EventBus.`default`.register { (x: DoSomeThingMessage) in
    // handle DoSomeThingMessage
    print(x)
}

可能有人会说,啥玩意,这不就是把NSNotificationName的职责让这个MessageWapper来做了么?有什么区别?最大的区别是,我们利用MessageWapper来实现消息类型的区分之后,可以利用Swift本身的类型系统来确保事件的唯一性以及在保持可读性的前提之下规避了冗杂繁复的NSNotificationName。如果有人在同一个模块内取名了同一个消息类型,那么编译器可以告诉你发生了重复,这是原来字符串所做不到的事情吗,而如果在不同的模块内取名了同一个消息类型,那么由于是在不同的Swift模块中定义的,因此这两个消息类型也会被完美的区分开来。

2. 事件分发

SwiftyEventBus中,事件分发的方式采用了闭包的方式,这样的好处是SwiftyEventBus可以在纯Swift的环境之下运行,不会出现如果没有OC运行环境而造成的通知机制的缺失。值得注意的是,在EventBus.default.register方法调用之后,会返回一个类型为EventSubscription的存根,它的作用是将自身观察者的生命周期绑定在一个实例之上,如果这个实例被销毁了,那么也就没有再继续观察的必要了,这样的好处是,我们不需要像NSNotification那样显示的调用remove方法来移除观察者,虽然新版本的iOS已经不需要显示的移除,但是为了对老版本iOS的兼容,一般情况之下我们还是需要进行显示的移除,这样的设计也是非常的不友好的。

3. 类型安全

除此之外,由于闭包所接受的类型在编译的时候已经确定了,所以我们也不需要显示的类型转换,我们也可以保证代码的类型安全,不会存在NSNotificationCenterid类型的困惑,这也是Swift所倡导的类型安全机制,SwiftyEventBus充分的利用了这一点,即便在后期的代码重构过程中变更了数据的类型,我们也可以在编译期就提前发现问题,从而保障代码的鲁棒性。

4. 高级特性

SwiftEventBus还提供了一系列的高级特性:

1.线程切换
EventBus.default.register(on: .main, messageEvent: { (x: String) in
    /// do something with x
    print(x)
})

这样我们无论在哪个线程进行消息的发送,我们总能在主线程进行相关的处理。

2.优先级控制
EventBus.default.register(priority: .low, messageEvent: { (x: String) in
    /// do something with x
    print(x)
})

EventBus.default.register(priority: .high, messageEvent: { (x: String) in
    /// do something with x
    print(x)
})

对于上述的例子来说,如果发送了一个消息都将被两个观察者接收,那么优先级高的观察者将率先接收到数据,处理完成之后才能轮到低优先级的观察者接受。

3.粘性事件(Sticky Event)

在安卓的EventBus中有一个非常常用的特性就是Sticky Event,在SwiftyEventBus中也实现了这一特性,简单的说,EventBus会记录下最近一次的事件,然后在观察者进行订阅的时候进行回放。

let bus = EventBus(domain: stickyFlag)
bus.stick.post("foo")
bus.stick.register(on: .main, priority: .default, messageEvent: { (x: String) in
    /// do something with x
    print(x)
}).release(by: self.box)

按照正常来说,如果在注册观察者之前进行消息的发送,那么在之后注册观察者的时候不会接收到相关的信息,但是由于是sticky事件,所以我们可以接收到这个事件,从而进行相关的处理。

4.Rx拓展

此外,如果你使用的是RxSwift,你也可以无缝的接入到Rx的世界中:

EventBus.default.rx.register(String.self)
    .subscribe(onNext: { (x) in
       expect(x).to(equal("foo"))
       done()
    })
    .disposed(by: bag!)

内存优化

在最开始的实现版本中,使用的是原生的Dictionary来实现类型以及观察者的对应,这样简单的实现固然是可以实现功能,但是如果对Swift原生的Dictionary有一定的了解的话,我们会发现这样可能存在一些内存浪费的问题。

Swift中Dictionay的扩容

在Swift中,Dictionay的原生实现内部是采用了_HashTable这个数据结构,当这个_HashTable是可变数据类型的时候,一旦达到容量的阈值就会发生一次扩容操作,而扩容操作会进行大量的内存拷贝,此外,也会造成无意义的内存的占用,这种情况在大量的数据的情况之下更为明显,因此我们可以进行一些内存上的优化。

internal static func capacity(forScale scale: Int8) -> Int {
    let bucketCount = (1 as Int) &<< scale
    return Int(Double(bucketCount) * maxLoadFactor)
}

最后的优化方案参考了JDK中的HashTable的方案,也就是说通过列表以及哈希表的方式来防止Swift中可变容器类型的内存突变,从而达到减少内存使用的目的,当然如果想更加进一步的优化查找的性能,也可以使用红黑树。

总结

SwiftyEventBus主要满足了我对事件总线分发机制在Swift中的幻想,当然可能有很多不成熟的地方,欢迎大家指正以及交流,项目的开源地址在这里

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

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,096评论 1 32
  • 项目到了一定阶段会出现一种甜蜜的负担:业务的不断发展与人员的流动性越来越大,代码维护与测试回归流程越来越繁琐。这个...
    fdacc6a1e764阅读 3,179评论 0 6
  • EventBus源码分析(一) EventBus官方介绍为一个为Android系统优化的事件订阅总线,它不仅可以很...
    蕉下孤客阅读 3,992评论 4 42
  • 第二次世界大战期间,德国有一家名不见经传的信托公司叫巴比纳信托行,专为顾客保管贵重物品。 战争爆发后,人们纷纷取走...
    牛犁阅读 1,810评论 36 46
  • 本不想写这段特别囧的经历,但是考虑很久,觉得还是要记录一下第一次误机的深刻教训! 狗血的事情通常都有着美好的开始!...
    健人姐姐阅读 919评论 0 9