再造蟲洞:一次 Objective-C 到 Swift 的改寫之旅
再造蟲洞:一次 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 就只是基本的改寫,並無介紹的必要,有興趣的讀者請自行閱讀程式碼,地址為: http://github.com/nixzhu/Wormhole 。
歡迎轉載,但請一定註明出處! http://github.com/nixzhu/dev-blog
- Swift 中 Actor 的重入問題
- 測試五要素
- App的環境
- 解析器組合子
- 能偶爾用上的 awk
- 製作一個苦力
- NSCache 原始碼分析
- 一種頭像快取策略
- 國產SDK
- 對函數語言程式設計的一點理解
- 處理鍵盤通知
- 再造蟲洞:一次 Objective-C 到 Swift 的改寫之旅
- 區別 iPhone 做佈局
- 用 Swift 實現輕量的屬性監聽系統
- 使用狀態機的好處
- 生成自適應圖表圖片的祕密
- AsyncDisplayKit 教程:達到 60 FPS 的滾動幀率
- 使用 Swift 構建自定義(且“可設計”的)控制元件
- 使用 Swift 和 Xcode 6 製作超棒的 UI 元件
- 介紹 MVVM