即使一行 Objective-C 代碼也不寫,每一個 Swift app 都會在 Objective-C runtime 中運行,開啟動態任務分發和運行時對象關聯的世界。更確切地說,可能在僅使用 Swift 庫的時候只運行 Swift runtime。但 Objective-C runtime 與我們共處了如此長的時間,我們也應該將其發揮到極致。
本周的 NShipster 我們將以 Swift 視角來觀察這兩個運行時中關於關聯對象(associated objects)和方法交叉(method swizzling)的技術。
提醒: 本文主要從 Swift 角度講這兩種技術,如果需要更詳細的解釋,請參考上述兩篇原文。
關聯對象(Associated Objects)
Swift extension 能對已經存在 Cocoa 類中添加極為豐富的功能,但它的兄弟 Objective-C 的 category 卻遜色了不少。比如說 Objective-C 中的 extension 就無法向既有類添加屬性。
令人慶幸的是 Objective-C 的 關聯對象 可以緩解這種局面。例如要向一個工程裡所有的 view controllers 中添加一個 descriptiveName 屬性,我們可以簡單的使用 objc_get/setAssociatedObject()來填充其 get 和 set 塊:
Swift
extension UIViewController { private struct AssociatedKeys { static var DescriptiveName = "nsh_DescriptiveName" } var descriptiveName: String? { get { return objc_getAssociatedObject(self, &AssociatedKeys.DescriptiveName) as? String } set { if let newValue = newValue { objc_setAssociatedObject( self, &AssociatedKeys.DescriptiveName, newValue as NSString?, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC) ) } } } }
注意,在私有嵌套 struct 中使用 static var,這樣會生成我們所需的關聯對象鍵,但不會污染整個命名空間。
方法交叉(Method Swizzling)
有時為了方便,也有可能是解決某些框架內的 bug,或者別無他法時,需要修改一個已經存在類的方法的行為。方法交叉可以讓你交換兩個方法的實現,相當於是用你寫的方法來重載原有方法,並且還能夠是原有方法的行為保持不變。
這個例子中我們交叉 UIViewController 的 viewWillAppear 方法以打印出每一個在屏幕上顯示的 view。方法交叉發生在 initialize 類方法調用時(如下代碼所示);替代的實現在 nsh_viewWillAppear 方法中:
Swift
extension UIViewController { public override class func initialize() { struct Static { static var token: dispatch_once_t = 0 } // make sure this isn't a subclass if self !== UIViewController.self { return } dispatch_once(&Static.token) { let originalSelector = Selector("viewWillAppear:") let swizzledSelector = Selector("nsh_viewWillAppear:") let originalMethod = class_getInstanceMethod(self, originalSelector) let swizzledMethod = class_getInstanceMethod(self, swizzledSelector) let didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)) if didAddMethod { class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)) } else { method_exchangeImplementations(originalMethod, swizzledMethod); } } } // MARK: - Method Swizzling func nsh_viewWillAppear(animated: Bool) { self.nsh_viewWillAppear(animated) if let name = self.descriptiveName { println("viewWillAppear: \(name)") } else { println("viewWillAppear: \(self)") } } }
load vs. initialize (Swift 版本)
Objective-C runtime 理論上會在加載和初始化類的時候調用兩個類方法: load and initialize。在講解 method swizzling 的原文中 Mattt 老師指出出於安全性和一致性的考慮,方法交叉過程 永遠 會在 load() 方法中進行。每一個類在加載時只會調用一次 load 方法。另一方面,一個 initialize 方法可以被一個類和它所有的子類調用,比如說 UIViewController 的該方法,如果那個類沒有被傳遞信息,那麼它的 initialize 方法就永遠不會被調用了。
不幸的是,在 Swift 中 load 類方法永遠不會被 runtime 調用,因此方法交叉就變成了不可能的事。但我們還有兩個辦法:
在 initialize 中實現方法交叉 這種做法很安全,你只需要確保相關的方法交叉在一個 dispatch_once 中就好了(這也是最推薦的做法)。
在 app delegate 中實現方法交叉 不像上面通過類擴展進行方法交叉,而是簡單地在 app delegate 的 application(_:didFinishLaunchingWithOptions:) 方法調用時中執行相關代碼也是可以的。基於對類的修改,這種方法應該就足夠確保這些代碼會被執行到。
最後,請記住僅在不得已的情況下使用 Objective-C runtime。隨便修改基礎框架或所使用的三方代碼是毀掉你的應用的絕佳方法哦。請務必要小心哦。