Skip to content

Latest commit

 

History

History
237 lines (154 loc) · 10.5 KB

2015-05-27-wormhole.md

File metadata and controls

237 lines (154 loc) · 10.5 KB

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

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

作者:@nixzhu

=================================

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

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

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

分析

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

首先是初始化:

- (instancetype)initWithApplicationGroupIdentifier:(NSString *)identifier
                                 optionalDirectory:(NSString *)directory

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

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

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

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

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

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

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

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

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

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

另外还有三个方法:

- (id)messageWithIdentifier:(NSString *)identifier;

- (void)clearMessageContentsForIdentifier:(NSString *)identifier;

- (void)clearAllMessageContents;

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

改写

先定 API 如下:

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 了。至于具体的实现,首先是一些类型定义:

typealias Message = NSCoding

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

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:

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

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 的实现如下:

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)


        // Try fire Listener's action for first time

        listener.action(messageWithIdentifier(identifier))
    }
}

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

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

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 安装它:

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 以分享此文!

如果你认为这篇文章不错,也有闲钱,那你可以用支付宝扫描下方二维码随便捐助一点,以慰劳作者的辛苦:

nixzhu的支付宝二维码