如何在 Swift 中高效地使用 Method Swizzling

做者: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 方法的實現會被替換成 initializenewViewWillAppear 方法的實現。值得注意的是,在 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

要在 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 對象第一個方法被調用前,methodOnemethodTwo 的實現被相互替換了。

結束語

正如你看到的,在 Swift 中仍是可使用 Method Swizzling 的,只是個人意見是,絕大部分時候不該該出如今實際的產品代碼裏。若是你想經過 swizzling 快速解決問題的話,那更好的辦法就是想出一個更好的架構,重構你的代碼。

本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 http://swift.gg

相關文章
相關標籤/搜索