既然 Swift 是未来,那手工将一些 Objective-C 的代码转成 Swift 就挺有必要。但如果只是简单的改写,而不使用 Swift 的特点,这个过程就会变得乏味。改写应当是一种再思考、再设计的过程。
作者:@nixzhu
=================================
我第一次知道有个叫 MMWormhole 的项目时,这个名字让我很是激动。又了解到它主要用于 iOS 扩展与主应用的实时通信,更让这个名字十分合理。因为物理上的虫洞就是一种能让我们不受空间的限制,远距离传递信息的超空间通道。
于是我去看它的代码,奇怪的是它为何用 Objective-C 写成。按理说,Swift 的发布也有一段时间了。这个项目又是用于扩展和主应用的通信,而且主要是为 WATCH 和对应 iPhone 应用的通信,用 Swift 来写不是更具有未来感吗?
因此我想到去改写,并看看用 Swift 去实现它是否会遇到困难。
分析
MMWormhole 的主要代码不过 300 行,看起来很容易,因此先分析一下它的 API
首先是初始化:
1 2
| - (instancetype)initWithApplicationGroupIdentifier:(NSString *)identifier optionalDirectory:(NSString *)directory
|
它接收一个 App Group ID 和一个可选的目录名,表明了虫洞的实现需要 App Group 的支持。这很好理解,因为 iOS 应用的扩展和主应用并不在同一个沙盒内,要让它们通信,只能用 App Group,或者网络。
有了虫洞,就可以往里面传递消息:
1 2
| - (void)passMessageObject:(id <NSCoding>)messageObject identifier:(NSString *)identifier;
|
它需要消息的名字,以及一个满足 NSCoding 协议的对象。具其文档解释,是因为它使用了 NSKeyedArchiver 来作为序列化媒介来将 messageObject 存储于 App Group 所在的文件系统里,以便虫洞的另一端读取。在实现上,为了及时性,它会使用 Darwin Notify Center 来发送一个名为 identifier 的通知。这种通知作用于整个系统范围,因此可以在 Extension 与 Container App 之间通信,但接收端必须处于 awake 状态。
有了传递,自然就有接收:
1 2
| - (void)listenForMessageWithIdentifier:(NSString *)identifier listener:(void (^)(id messageObject))listener;
|
这个方法监听特定的消息,并在听到时执行一个 block,很好理解。而且很明显,我们可以为一种消息增加多个监听者。只要多调用这个方法几次即可。
有了监听,就该有取消监听:
1
| - (void)stopListeningForMessageWithIdentifier:(NSString *)identifier;
|
但很遗憾,这个方法会移除所有监听此消息的监听者,而不能单个的移除。如果我们要用 Swift 改写,这该是一个可以改进的地方。
另外还有三个方法:
1 2 3 4 5
| - (id)messageWithIdentifier:(NSString *)identifier; - (void)clearMessageContentsForIdentifier:(NSString *)identifier; - (void)clearAllMessageContents;
|
分别用于根据消息的 ID 获取消息对象(在初始化时很有用,可以获取“过去”的消息),以及从文件系统中清除消息对象(单个,或全部)
改写
先定 API 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| init(appGroupIdentifier: String, messageDirectoryName: String) func passMessage(message: Message?, withIdentifier identifier: String) func bindListener(listener: Listener, forMessageWithIdentifier identifier: String) func removeListener(listener: Listener, forMessageWithIdentifier identifier: String) func removeListenerByName(name: String, forMessageWithIdentifier identifier: String) func removeAllListenersForMessageWithIdentifier(identifier: String) func messageWithIdentifier(identifier: String) -> Message? func destroyMessageWithIdentifier(identifier: String) func destroyAllMessages()
|
除了 API 的命名外,并无太大区别。只是现在我们可以为某个消息移除单个 Listener 了。至于具体的实现,首先是一些类型定义:
1
| typealias Message = NSCoding
|
将 Message 作为 NSCoding 的别名,非常直观。然后是 Listener:
1 2 3 4 5 6 7 8 9 10 11 12
| struct Listener { typealias Action = Message? -> Void let name: String let action: Action init(name: String, action: Action) { self.name = name self.action = action } }
|
Listener 有一个名字和一个操作。这也是有别于 MMWormhole 的地方,它的 listener 只是一个 block,相当于这里的 action,而没有名字,因此无法单独移除。
接下来我们实现 passMessage:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| func passMessage(message: Message?, withIdentifier identifier: String) { if identifier.isEmpty { fatalError("ERROR: Message need identifier") } if let message = message { var success = false if let filePath = filePathForIdentifier(identifier) { let data = NSKeyedArchiver.archivedDataWithRootObject(message) success = data.writeToFile(filePath, atomically: true) } if success { if let center = CFNotificationCenterGetDarwinNotifyCenter() { CFNotificationCenterPostNotification(center, identifier, nil, nil, 1) } } } else { if let center = CFNotificationCenterGetDarwinNotifyCenter() { CFNotificationCenterPostNotification(center, identifier, nil, nil, 1) } } }
|
也很简单,首先确保消息的 identifier 不为空,不然接收端没办法区别不同的消息。然后根据消息主体的有无(有时候我们只需要 identifier 即可)来决定 CFNotificationCenterPostNotification 的时机,如有,就生成一个 filePath 并用 NSKeyedArchiver 将消息压缩为 NSData 在写入文件,在保证成功的前提下发送通知;如无,直接发送通知。
然后是实现 bindListener,这是真正的考验,因为 CFNotificationCenterAddObserver
1 2 3 4 5 6 7 8
| void CFNotificationCenterAddObserver ( CFNotificationCenterRef center, const void *observer, CFNotificationCallback callBack, CFStringRef name, const void *object, CFNotificationSuspensionBehavior suspensionBehavior );
|
需要的参数中的第三个 CFNotificationCallback 是函数指针,而 Swift (1.2) 还不能创建函数指针。基本上,这就会强制你写 Objective-C 代码,这也解决了之前的疑惑,为何 MMWormhole 用 Objective-C 来写。很明显,既然具体的实现离不开 Objective-C,那不妨全部用 Objective-C 来写。
但是(是的,世界上充满了但是)我还不打算放弃,因为在 Swift 中依然可以使用 Objective-C 的运行时。通过它,也许我们不需要显式的 Objective-C 代码就能构造出一个函数指针来。
根据这篇文章提到的一种 hack 方法(也就意味着有风险),我们可以将一个 Swift 的闭包转换为一个某个对象的 IMP,而 IMP 正是函数指针的一个别名。因此,bindListener 的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| func bindListener(listener: Listener, forMessageWithIdentifier identifier: String) { if let center = CFNotificationCenterGetDarwinNotifyCenter() { let messageListener = MessageListener(messageIdentifier: identifier, listener: listener) messageListenerSet.insert(messageListener) let block: @objc_block (CFNotificationCenter!, UnsafeMutablePointer<Void>, CFString!, UnsafePointer<Void>, CFDictionary!) -> Void = { _, _, _, _, _ in if self.messageListenerSet.contains(messageListener) { messageListener.listener.action(self.messageWithIdentifier(identifier)) } } let imp: COpaquePointer = imp_implementationWithBlock(unsafeBitCast(block, AnyObject.self)) let callBack: CFNotificationCallback = unsafeBitCast(imp, CFNotificationCallback.self) CFNotificationCenterAddObserver(center, unsafeAddressOf(self), callBack, identifier, nil, CFNotificationSuspensionBehavior.DeliverImmediately) listener.action(messageWithIdentifier(identifier)) } }
|
之所以一定要实现这个 callBack,是因为我们必须在这个 callBack 里调用我们的 Listener 的 Action 闭包以便执行使用此消息的一些操作。另外请注意 block 的形式参数都是 _, _, _, _, _,
一半原因是我的实现不需要使用到它们,另一半原因是这终究是一种 hack 的方法,也许有失效的一天,而不使用其参数可能减轻不利影响。
需要注意的是,在 Wormhole 内部,我增加了一个 MessageListener:
1 2 3 4 5 6 7 8 9 10 11 12 13
| func ==(lhs: Wormhole.MessageListener, rhs: Wormhole.MessageListener) -> Bool { return lhs.hashValue == rhs.hashValue } struct MessageListener: Hashable { let messageIdentifier: String let listener: Listener var hashValue: Int { return (messageIdentifier + "<nixzhu.Wormhole>" + listener.name).hashValue } }
|
用于封装 Listener 和 messageIdentifier。而且它满足 Hashable 协议,这样用集合 var messageListenerSet = Set<MessageListener>()
来装载所有的 MessageListener 就能带来好处:方便判断 Listener 的有效性,自动更新监听同一个 Message 的同名 Listener,也可以单独移除某一个 Listener。
除了 removeListener 外,其它的 API 就只是基本的改写,并无介绍的必要,有兴趣的读者请自行阅读代码,地址为:https://github.com/nixzhu/Wormhole。
如果你最近要开发 WatchKit 应用,需要使用到 Wormhole 所提供的功能,那可以用 pod 安装它:
1 2 3 4 5
| source 'https://github.com/CocoaPods/Specs.git' platform :ios, '8.0' use_frameworks! pod 'Wormhole'
|
===============
欢迎转载,但请一定注明出处! https://github.com/nixzhu/dev-blog
欢迎转发此条 Tweet https://twitter.com/nixzhu/status/603433116263272448 或微博 http://weibo.com/2076580237/CjLoYuQgR 以分享此文!
如果你认为这篇文章不错,也有闲钱,那你可以用支付宝扫描下方二维码随便捐助一点,以慰劳作者的辛苦: