再造虫洞:一次 Objective-C 到 Swift 的改写之旅

既然 Swift 是未来,那手工将一些 Objective-C 的代码转成 Swift 就挺有必要。但如果只是简单的改写,而不使用 Swift 的特点,这个过程就会变得乏味。改写应当是一种再思考、再设计的过程。

作者:@nixzhu


我第一次知道有个叫 MMWormhole 的项目时,这个名字让我很是激动。又了解到它主要用于 iOS 扩展与主应用的实时通信,更让这个名字十分合理。因为物理上的虫洞就是一种能让我们不受空间的限制,远距离传递信息的超空间通道。

于是我去看它的代码,奇怪的是它为何用 Objective-C 写成。按理说,Swift 的发布也有一段时间了。这个项目又是用于扩展和主应用的通信,而且主要是为 WATCH 和对应 iPhone 应用的通信,用 Swift 来写不是更具有未来感吗?

因此我想到去改写,并看看用 Swift 去实现它是否会遇到困难。

分析

MMWormhole 的主要代码不过 300 行,看起来很容易,因此先分析一下它的 API

首先是初始化:

  1. - (instancetype)initWithApplicationGroupIdentifier:(NSString *)identifier
  2. optionalDirectory:(NSString *)directory

它接收一个 App Group ID 和一个可选的目录名,表明了虫洞的实现需要 App Group 的支持。这很好理解,因为 iOS 应用的扩展和主应用并不在同一个沙盒内,要让它们通信,只能用 App Group,或者网络。

有了虫洞,就可以往里面传递消息:

  1. - (void)passMessageObject:(id <NSCoding>)messageObject
  2. identifier:(NSString *)identifier;

它需要消息的名字,以及一个满足 NSCoding 协议的对象。具其文档解释,是因为它使用了 NSKeyedArchiver 来作为序列化媒介来将 messageObject 存储于 App Group 所在的文件系统里,以便虫洞的另一端读取。在实现上,为了及时性,它会使用 Darwin Notify Center 来发送一个名为 identifier 的通知。这种通知作用于整个系统范围,因此可以在 Extension 与 Container App 之间通信,但接收端必须处于 awake 状态。

有了传递,自然就有接收:

  1. - (void)listenForMessageWithIdentifier:(NSString *)identifier
  2. listener:(void (^)(id messageObject))listener;

这个方法监听特定的消息,并在听到时执行一个 block,很好理解。而且很明显,我们可以为一种消息增加多个监听者。只要多调用这个方法几次即可。

有了监听,就该有取消监听:

  1. - (void)stopListeningForMessageWithIdentifier:(NSString *)identifier;

但很遗憾,这个方法会移除所有监听此消息的监听者,而不能单个的移除。如果我们要用 Swift 改写,这该是一个可以改进的地方。

另外还有三个方法:

  1. - (id)messageWithIdentifier:(NSString *)identifier;
  2. - (void)clearMessageContentsForIdentifier:(NSString *)identifier;
  3. - (void)clearAllMessageContents;

分别用于根据消息的 ID 获取消息对象(在初始化时很有用,可以获取“过去”的消息),以及从文件系统中清除消息对象(单个,或全部)

改写

先定 API 如下:

  1. init(appGroupIdentifier: String, messageDirectoryName: String)
  2. func passMessage(message: Message?, withIdentifier identifier: String)
  3. func bindListener(listener: Listener, forMessageWithIdentifier identifier: String)
  4. func removeListener(listener: Listener, forMessageWithIdentifier identifier: String)
  5. func removeListenerByName(name: String, forMessageWithIdentifier identifier: String)
  6. func removeAllListenersForMessageWithIdentifier(identifier: String)
  7. func messageWithIdentifier(identifier: String) -> Message?
  8. func destroyMessageWithIdentifier(identifier: String)
  9. func destroyAllMessages()

除了 API 的命名外,并无太大区别。只是现在我们可以为某个消息移除单个 Listener 了。至于具体的实现,首先是一些类型定义:

  1. typealias Message = NSCoding

将 Message 作为 NSCoding 的别名,非常直观。然后是 Listener:

  1. struct Listener {
  2. typealias Action = Message? -> Void
  3. let name: String
  4. let action: Action
  5. init(name: String, action: Action) {
  6. self.name = name
  7. self.action = action
  8. }
  9. }

Listener 有一个名字和一个操作。这也是有别于 MMWormhole 的地方,它的 listener 只是一个 block,相当于这里的 action,而没有名字,因此无法单独移除。

接下来我们实现 passMessage:

  1. func passMessage(message: Message?, withIdentifier identifier: String) {
  2. if identifier.isEmpty {
  3. fatalError("ERROR: Message need identifier")
  4. }
  5. if let message = message {
  6. var success = false
  7. if let filePath = filePathForIdentifier(identifier) {
  8. let data = NSKeyedArchiver.archivedDataWithRootObject(message)
  9. success = data.writeToFile(filePath, atomically: true)
  10. }
  11. if success {
  12. if let center = CFNotificationCenterGetDarwinNotifyCenter() {
  13. CFNotificationCenterPostNotification(center, identifier, nil, nil, 1)
  14. }
  15. }
  16. } else {
  17. if let center = CFNotificationCenterGetDarwinNotifyCenter() {
  18. CFNotificationCenterPostNotification(center, identifier, nil, nil, 1)
  19. }
  20. }
  21. }

也很简单,首先确保消息的 identifier 不为空,不然接收端没办法区别不同的消息。然后根据消息主体的有无(有时候我们只需要 identifier 即可)来决定 CFNotificationCenterPostNotification 的时机,如有,就生成一个 filePath 并用 NSKeyedArchiver 将消息压缩为 NSData 在写入文件,在保证成功的前提下发送通知;如无,直接发送通知。

然后是实现 bindListener,这是真正的考验,因为 CFNotificationCenterAddObserver

  1. void CFNotificationCenterAddObserver (
  2. CFNotificationCenterRef center,
  3. const void *observer,
  4. CFNotificationCallback callBack,
  5. CFStringRef name,
  6. const void *object,
  7. CFNotificationSuspensionBehavior suspensionBehavior
  8. );

需要的参数中的第三个 CFNotificationCallback 是函数指针,而 Swift (1.2) 还不能创建函数指针。基本上,这就会强制你写 Objective-C 代码,这也解决了之前的疑惑,为何 MMWormhole 用 Objective-C 来写。很明显,既然具体的实现离不开 Objective-C,那不妨全部用 Objective-C 来写。

但是(是的,世界上充满了但是)我还不打算放弃,因为在 Swift 中依然可以使用 Objective-C 的运行时。通过它,也许我们不需要显式的 Objective-C 代码就能构造出一个函数指针来。

根据这篇文章提到的一种 hack 方法(也就意味着有风险),我们可以将一个 Swift 的闭包转换为一个某个对象的 IMP,而 IMP 正是函数指针的一个别名。因此,bindListener 的实现如下:

  1. func bindListener(listener: Listener, forMessageWithIdentifier identifier: String) {
  2. if let center = CFNotificationCenterGetDarwinNotifyCenter() {
  3. let messageListener = MessageListener(messageIdentifier: identifier, listener: listener)
  4. messageListenerSet.insert(messageListener)
  5. let block: @objc_block (CFNotificationCenter!, UnsafeMutablePointer<Void>, CFString!, UnsafePointer<Void>, CFDictionary!) -> Void = { _, _, _, _, _ in
  6. if self.messageListenerSet.contains(messageListener) {
  7. messageListener.listener.action(self.messageWithIdentifier(identifier))
  8. }
  9. }
  10. let imp: COpaquePointer = imp_implementationWithBlock(unsafeBitCast(block, AnyObject.self))
  11. let callBack: CFNotificationCallback = unsafeBitCast(imp, CFNotificationCallback.self)
  12. CFNotificationCenterAddObserver(center, unsafeAddressOf(self), callBack, identifier, nil, CFNotificationSuspensionBehavior.DeliverImmediately)
  13. // Try fire Listener's action for first time
  14. listener.action(messageWithIdentifier(identifier))
  15. }
  16. }

之所以一定要实现这个 callBack,是因为我们必须在这个 callBack 里调用我们的 Listener 的 Action 闭包以便执行使用此消息的一些操作。另外请注意 block 的形式参数都是 _, _, _, _, _, 一半原因是我的实现不需要使用到它们,另一半原因是这终究是一种 hack 的方法,也许有失效的一天,而不使用其参数可能减轻不利影响。

需要注意的是,在 Wormhole 内部,我增加了一个 MessageListener:

  1. func ==(lhs: Wormhole.MessageListener, rhs: Wormhole.MessageListener) -> Bool {
  2. return lhs.hashValue == rhs.hashValue
  3. }
  4. struct MessageListener: Hashable {
  5. let messageIdentifier: String
  6. let listener: Listener
  7. var hashValue: Int {
  8. return (messageIdentifier + "<nixzhu.Wormhole>" + listener.name).hashValue
  9. }
  10. }

用于封装 Listener 和 messageIdentifier。而且它满足 Hashable 协议,这样用集合 var messageListenerSet = Set<MessageListener>() 来装载所有的 MessageListener 就能带来好处:方便判断 Listener 的有效性,自动更新监听同一个 Message 的同名 Listener,也可以单独移除某一个 Listener。

除了 removeListener 外,其它的 API 就只是基本的改写,并无介绍的必要,有兴趣的读者请自行阅读代码,地址为:https://github.com/nixzhu/Wormhole


欢迎转载,但请一定注明出处! https://github.com/nixzhu/dev-blog