前言
由于最近划水的厉害,
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. 事件区分
首先从使用上来说更加方便使用了,我们不需要再为通知取名而费尽心思,只需要简单的register
和post
就可以完成整套的操作,当然在现实世界中不建议直接传递基本数据类型,这样会损坏通知的表达能力,建议可以这么做:
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. 类型安全
除此之外,由于闭包所接受的类型在编译的时候已经确定了,所以我们也不需要显示的类型转换,我们也可以保证代码的类型安全,不会存在NSNotificationCenter
中id
类型的困惑,这也是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
中的幻想,当然可能有很多不成熟的地方,欢迎大家指正以及交流,项目的开源地址在这里。