做者:uraimo,原文連接,原文日期:2015-10-23
譯者:小鐵匠Linus;校對:100mango;定稿:Channehtml
示例程序能夠從 Github 上下載。ios
(譯者注:譯者也寫了一篇關於 Method Swizzling 的文章 Runtime 之 Method Swizzling,順便寫了兩個小 demo 來對比 Objective-C 和 Swift 之間 Method Swizzling 的異同,示例代碼傳送門)git
Method Swizzling 在 Objective-C 或其餘語言中是一種頗有名的技術,用來支持動態方法派發。github
Method Swizzling 經過改變特定 selector(方法)與實際實現之間的映射,在 runtime 時將一個方法的實現替換成其它方法的實現。objective-c
雖然這看起來很是方便,可是這個功能也有其缺點。在 runtime 執行這類更改時,你就不能在編譯時利用那些可用的安全檢查了。所以,應該當心使用 Method Swizzling。swift
NSHipster 上有一篇關於如何在 Objective-C 中使用 Method Swizzling 的文章(譯者注:南峯子的技術博客有這篇文章的譯文)(其餘詳情也能夠看這裏)以及 Stackoverflow 上有一些如何使用 Method Swizzling 的討論。安全
Swift 關於方法派發是使用靜態方法的,但有些情形可能須要用到 Method Swizzling。架構
在 Swift 中使用 Method Swizzling 以前,讓我再次重申一下這種技術仍是儘可能少用,只有當你的問題不能用 Swift 的方式解決,也不能用子類、協議或擴展解決時才使用。app
正如 NSHipster 上另外一篇文章描述的那樣,在 Swift 中對一個來自基本框架(Foundation、UIKit 等)的類使用 Method Swizzling 與 Objective-C 沒什麼區別。框架
extension UIViewController { public override static func initialize() { struct Static { static var token: dispatch_once_t = 0 } // 確保不是子類 if self !== UIViewController.self { return } dispatch_once(&Static.token) { let originalSelector = Selector("viewWillAppear:") let swizzledSelector = Selector("newViewWillAppear:") 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 newViewWillAppear(animated: Bool) { self.newViewWillAppear(animated) if let name = self.descriptiveName { print("viewWillAppear: \(name)") } else { print("viewWillAppear: \(self)") } } }
在這個例子中,應用中的每一個 UIViewController 都會執行額外的操做,而原始的 viewWillAppear
方法會被保護起來不執行,這種情形只能經過 Method Swizzling 來實現。
viewWillAppear
方法的實現會被替換成 initialize
中 newViewWillAppear
方法的實現。值得注意的是,在 swizzle 後,會遞歸調用 newViewWillAppear
方法,以替換以前原始的 viewWillAppear
方法。
與 Objective-C 的第一個不一樣之處就是 swizzling 不在 load
方法裏執行。
加載一個類的定義時,會調用 load
方法,所以這地方適合執行 Method Swizzling 。
可是 load
方法只在 Objective-C 裏有,並且不能在 Swift 裏重載,無論怎麼試都會報編譯錯誤。接下來執行 swizzle 最好的地方就是 initialize
了,這是調用你的類中第一個方法前的地方。
全部對修改方法執行的操做放在 dispatch_once
中,這是爲了確保這個過程只執行一次。
這些都是你要知道的關於繼承自基本框架或 Objective-C 橋接類的。若是你要在純 Swift 類中正確使用 Method Swizzling 的話,就須要將下面這些注意事項記在心上。
要在 Swift 自定義類中使用 Method Swizzling 有兩個必要條件:
包含 swizzle 方法的類須要繼承自 NSObject
須要 swizzle 的方法必須有動態屬性(dynamic attribute)
更多關於爲何必須須要這些能夠在蘋果的文檔裏查看:
須要動態派發
當 @objc 屬性將你的 Swift API 暴露給 Objective-C runtime 時,不能確保屬性、方法、初始器的動態派發。Swift 編譯器可能爲了優化你的代碼,而繞過 Objective-C runtime。當你指定一個成員變量 爲 dynamic 時,訪問該變量就總會動態派發了。由於在變量定義時指定 dynamic 後,會使用 Objective-C runtime 來派發。
動態派發是必須的。可是,你必須使用 dynamic 修飾才能知道在運行時 API 的實現被替換了。舉個例子,你能夠在 Objective-C runtime 使用 method_exchangeImplementations
方法來交換兩個方法的實現。假如 Swift 編譯器將方法的實現內聯(inline)了或者訪問去虛擬化(devirtualize)了,這個交換過來的新的實現就無效了。
說白了,若是你想要替換的方法沒有聲明爲 dynamic 的話,就不能 swizzle。
翻譯成代碼就是這樣:
class TestSwizzling : NSObject { dynamic func methodOne()->Int{ return 1 } } extension TestSwizzling { //在 Objective-C 中,咱們在 load() 方法進行 swizzling。但Swift不容許使用這個方法。 override class func initialize() { struct Static { static var token: dispatch_once_t = 0; } // 只執行一次 dispatch_once(&Static.token) { let originalSelector = Selector("methodOne"); let swizzledSelector = Selector("methodTwo"); let originalMethod = class_getInstanceMethod(self, originalSelector); let swizzledMethod = class_getInstanceMethod(self, swizzledSelector); method_exchangeImplementations(originalMethod, swizzledMethod); } } func methodTwo()->Int{ // swizzling 後, 該方法就不會遞歸調用 return methodTwo()+1 } } var c = TestSwizzling() print(c.methodOne()) //2 print(c.methodTwo()) //1
這個簡單的例子中,在 TestSwizzling
對象第一個方法被調用前,methodOne
和 methodTwo
的實現被相互替換了。
正如你看到的,在 Swift 中仍是可使用 Method Swizzling 的,只是個人意見是,絕大部分時候不該該出如今實際的產品代碼裏。若是你想經過 swizzling 快速解決問題的話,那更好的辦法就是想出一個更好的架構,重構你的代碼。
本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg。