Method Swizzling 為什麼要先呼叫 class_addMethod?

Natai發表於2019-04-17

先上個 Swift 中的 demoMethod Swizzling

Swift 中的實現

其實 Swift 中實現原理和 OC 基本一致,只是蘋果爸爸不再允許在 Swift 中使用+load()+initialize()方法,這當然難不倒各種大神,那麼我就做次農夫山泉。。。

Swizzling

先抽取 swizzling 的實現到NSObject的擴充套件當中:

extension NSObject {
    static func swizzlingForClass(_ forClass: AnyClass, originalSelector: Selector, swizzledSelector: Selector) {
        guard let originalMethod = class_getInstanceMethod(forClass, originalSelector),
              let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector) else {
            return
        }
        
        let isAddSuccess = class_addMethod(forClass, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))
        if isAddSuccess {
            class_replaceMethod(forClass, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod)
        }
    }
}
複製程式碼

可以看到核心實現和 OC 是完全一致的,那麼剩下的就是模擬 OC 版本實現中的+load()dispatch_once

dispatch_once

我們用viewDidLoad來做個dispatch_once的示範:

extension UIViewController {
    static func swizzleViewDidLoad() {
        _ = self.swizzleMethod
    }
    
    @objc func swizzled_viewDidLoad() {
        swizzled_viewDidLoad()
        print("嘻嘻")
    }
    
    private static let swizzleMethod: Void = {
        let originalSelector = #selector(viewDidLoad)
        let swizzledSelector = #selector(swizzled_viewDidLoad)
        swizzlingForClass(UIViewController.self, originalSelector: originalSelector, swizzledSelector: swizzledSelector)
    }()
}
複製程式碼

在 Swift 中static let這樣宣告的變數其實已經用到dispatch_once,而且static自帶lazy屬性,要在封裝函式swizzleViewDidLoad被呼叫時候才呼叫。

+load()

OC 中+load()方法會在類被裝載時呼叫,確保需要用到的方法都是被 Swizzling 過的。Swift 中可以在AppDelegateinit方法中手動呼叫 swizzle 方法模擬+load()實現。

class AppDelegate: UIResponder, UIApplicationDelegate {
    override init() {
        super.init()
        UIViewController.swizzleViewDidLoad()
    }
}
複製程式碼

為什麼要先呼叫 class_addMethod?

class_addMethod這個方法是很容易被人忽視的,對於 Swizzling 一節中的程式碼,還有一種常見的寫法:

extension NSObject {
    static func swizzlingForClass(_ forClass: AnyClass, originalSelector: Selector, swizzledSelector: Selector) {
        guard let originalMethod = class_getInstanceMethod(forClass, originalSelector),
              let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector) else {
            return
        }

        method_exchangeImplementations(originalMethod, swizzledMethod)
    }
}
複製程式碼

這種方式就只是簡單的直接交換了originalMethodswizzledMethod。乍一看貌似沒有問題(其實最開始我絞盡腦汁也沒想清楚到底哪裡不對。。。),但是為什麼各路大神都是用的第一種方式呢?網上有種說法:

要先嚐試新增原 selector 是為了做一層保護,因為如果這個類沒有實現原始方法"originalSel" ,但其父類實現了,那 class_getInstanceMethod 會返回父類的方法。這樣 method_exchangeImplementations 替換的是父類的那個方法,這當然不是你想要的。所以我們先嚐試新增 originalSel ,如果已經存在,再用 method_exchangeImplementations 把原方法的實現跟新的方法實現給交換掉。

其實這種說法已經算是比較明確問題所在了,但是愚笨的我還是沒有想通到底為什就“這當然不是你想要的”了呢。

又是一番絞盡腦汁。。。終於 Biuer 的一下想通了

在舉栗子前引用一段對 Selectors、Methods 和 Implementations 理解:

理解 selector, method, implementation 這三個概念之間關係的最好方式是:在執行時,類(Class)維護了一個訊息分發列表來解決訊息的正確傳送。每一個訊息列表的入口是一個方法(Method),這個方法對映了一對鍵值對,其中鍵值是這個方法的名字 selector(SEL),值是指向這個方法實現的函式指標 implementation(IMP)。 Method swizzling 修改了類的訊息分發列表使得已經存在的 selector 對映了另一個實現 implementation,同時重新命名了原生方法的實現為一個新的 selector。

假設父類有個方法method,子類未重寫method方法,子類的中想要拿來替換的方法為swizzledMethod

  1. 用第二種方式進行方法交換

    • 在子類的例項中呼叫method方法時,確實按預期正常執行的
    • 在父類的例項中呼叫method方法時,就開始崩潰了。因為方法交換後,method方法的IMP其實和子類swizzledMethodIMP進行了交換,此時等同於父類呼叫子類方法,當然會崩潰。
  2. 用第一種方式進行方法交換

    class_addMethod先判斷了子類中是否有method方法

    • 如果有,則新增失敗,直接進行交換
    • 如果沒有,則新增成功,將swizzledMethodIMP賦值給method這個Selector,然後在將methodIMP(其實是父類中的實現)賦值給swizzledMethod這個Selector

相關文章