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

    再造虫洞:一次 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