Method Swizzle 是 Objc Runtime 提供的幾個黑科技之一, 它能夠讓我們在運行時替換已有方法來實現我們的一些需求。 但它在使用中也有一些需要注意的地方, 咱們來聊聊。
Method Swizzle 黑科技
相信有一些開發經驗的同學,都用到過 Objc Runtime 的 Method Swizzle。它的應用場景也有很多,其中比較典型的一個場景就是進行一些非侵入性的能力注入。 這麼說可能不夠直觀,下面就用一個實際例子說明這個問題。AFNetworking 大家應該比較熟悉。這是它裡面的一段代碼:
static inline void af_swizzleSelector(Class theClass, SEL originalSelector, SEL swizzledSelector) { Method originalMethod = class_getInstanceMethod(theClass, originalSelector); Method swizzledMethod = class_getInstanceMethod(theClass, swizzledSelector); method_exchangeImplementations(originalMethod, swizzledMethod); } static inline BOOL af_addMethod(Class theClass, SEL selector, Method method) { return class_addMethod(theClass, selector, method_getImplementation(method), method_getTypeEncoding(method)); } + (void)swizzleResumeAndSuspendMethodForClass:(Class)theClass { Method afResumeMethod = class_getInstanceMethod(self, @selector(af_resume)); Method afSuspendMethod = class_getInstanceMethod(self, @selector(af_suspend)); if (af_addMethod(theClass, @selector(af_resume), afResumeMethod)) { af_swizzleSelector(theClass, @selector(resume), @selector(af_resume)); } if (af_addMethod(theClass, @selector(af_suspend), afSuspendMethod)) { af_swizzleSelector(theClass, @selector(suspend), @selector(af_suspend)); } }
這是 AFNetworking 對 NSURLSessionTask 的一個 swizzle 替換。 af_swizzleSelector 和 af_addMethod這兩個方法是對 swizzle 函數調用做了個封裝。 主邏輯在 swizzleResumeAndSuspendMethodForClass 方法。 這個方法做的事情就是將 NSURLSessionTask 的 resume 和 suspend 方法做了替換。 替換的目的也很簡單, 就是在這兩個方法調用的時候發送通知。
首先調用 class_getInstanceMethod 得到我們自己的實例方法 afResumeMethod 和 afSuspendMethod。 然後調用 af_addMethod 嘗試將我們的實例方法添加到 NSURLSessionTask 中(注:這裡的 theClass 在實際運行時,就是 [NSURLSessionTask class])。
如果是第一次執行, af_addMethod 就會返回 YES, 然後分別將 af_resume 和 af_suspend 這兩個 Selector 添加到 theClass 方法列表中。 添加好方法後,再調用 af_swizzleSelector 方法, 分別將 af_resume 和 resume, 以及 af_suspend 和 suspend 的方法實現進行互換。
這樣,我們在調用 [NSURLSessionTask resume] 的時候, 其實調用的是 [NSURLSessionTask af_resume], 就是這麼個情況~
af_swizzleSelector 方法中,其實是 Runtime 的 method_exchangeImplementations 函數的一個封裝。 這也是大家常用的一個 swizzle 函數, 但正是它,會帶來一些副作用, 這個也是我們後面要討論的主題。 先記住它吧。
容易被忽略的副作用
上面咱們演示了一個 Runtime Swizzle 的整體流程。 可能有一部分同學在使用 Swizzle 的時候,會用到method_exchangeImplementations 方法。 剛才我也提到了,它會有一些副作用, 咱們繼續來看看吧。
我們還是按照同樣的方式進行方法替換:
@implementation MyObject - (int) my_quantity { return 12; } - (void)main { SKPayment *payment = [[SKPayment alloc] init]; NSLog(@"payment %i", payment.quantity); //輸出:1 Method myQuantity = class_getInstanceMethod([self class], @selector(my_quantity)); Method originalQuantity = class_getInstanceMethod([payment class], @selector(quantity)); method_exchangeImplementations(myQuantity, originalQuantity); NSLog(@"replaced %i", (int)payment.quantity); //輸出: 12 } @end
我們這裡將我們自己的 my_quantity 方法與 [SKPayment quantity] 進行替換, 並且兩次使用 NSLog 進行輸出。 這次我們兩次 NSLog 都得到了預期的結果。 在替換方法之前 payment.quantity 輸出的是 1。 在替換之後,輸出的是 my_quantity 的 12。
到此為止,看起來都沒有任何問題。 但是如果在方法替換後, 我們顯示的調用 my_quantity 就有可能有問題了:
NSLog(@"original %i", [self my_quantity]);
大家想想, 這時候這個方法調用會輸出什麼結果呢? 肯定不是 12, 因為它的方法實現已經和 SKPayment 中的交換了。 那麼是 1 嗎?
在我實際運行中, 既不是 12 也不是 1。 而是程序執行到這裡直接 Crash 了。 這時為什麼呢?
我們不妨將 my_quantity 稍微修改一下:
- (int) my_quantity { NSLog(@"%@", self); return 12; }
這裡我們用 NSLog 輸出了 self 的內容。 在調用這行代碼的時候:
//輸出 (SKPayment: 0x60000001e9b0)(此處用圓括號替換尖括號) NSLog(@"replaced %i", (int)payment.quantity); //輸出: 12
命令行中還輸出了
這就是 objc Runtime 的消息機制的原理。 簡單來說,我們調用任何方法,在 runtime 時候, 都會被轉換成 objc_msgSend() 調用。 我們上面的代碼, 在運行時其實就是這樣:
objc_msgSend(payment, @selector(quantity))
而大家知道,我們傳入的 @selector(quantity) 已經被剛才的 Swizzle 替換成了 @selector(my_quantity), 這個好理解。 但還有一點要強調, 就是每個方法中對 self 的引用, 其實引用的就是 objc_msgSend 的第一個參數。
也就是說,雖然我們的 Selector 被 Swizzle 過程替換掉了, 但 self 實例是沒有替換過來的。 這點對於我們的my_quantity 的實現不會有影響, 因為 my_quantity 方法裡面只是簡單的返回了一個數字而已。
但對於 SKPayment 對應的 quantity 方法的實現就有可能有問題了。 因為 [SKPayment quantity] 的實現會認為 self 是一個 SKPayment 實例, 但我們是以這個方式調用的:
NSLog(@"original %i", [self my_quantity]);
在運行時, 它會被轉換成這樣:
objc_msgSend(MyObject, @selector(my_quantity))
還是因為 @selector(my_quantity) 和 @selector(quantity) 被 Swizzle 了, 所以我們這次實際調用的方法是[SKPayment quantity]。 但 objc_msgSend 傳入的第一個參數是我們自己的 MyObject 實例, 而不是 SKPayment 的實例。
也就是說, 雖然我們通過 Swizzle 將方法調用映射到了 [SKPayment quantity] 上, 但我們給他的 self 實例是不對的。 就會產生這種非預期的結果了。
總結一下, method_exchangeImplementations 來達成的 Swizzle, 會有雙向效果。 除了我們的目標方法, 還需要注意我們自己被替換的方法的安全性。 否則就非常容易出現這種意料之外的結果。
更安全的做法
剛才說了 method_exchangeImplementations 的一些弊端之後, 咱們再來看看是不是有其他的替代方案呢? 答案是肯定的。 Runtime 還提供了另一種 Swizzle 函數 method_setImplementation。
還是以剛才實例來進行:
int my_quantity(id self, SEL _cmd) { return 12; } - (void)viewDidLoad { [super viewDidLoad]; SKPayment *payment = [[SKPayment alloc] init]; NSLog(@"payment %i", payment.quantity);// 輸出 1 Method originalQuantity = class_getInstanceMethod([payment class], @selector(quantity)); method_setImplementation(originalQuantity, (IMP) my_quantity); NSLog(@"replaced %i", (int)payment.quantity);//輸出 12 }
這次我們把 my_quantity 定義成了 C 函數。 method_setImplementation 接受兩個參數,第一個還是我們要替換的方法。 而第二個參數是一個 IMP 類型的。 其實 IMP 就是一個 C 函數了。 我們定義的 my_quantity 接受兩個參數, self 和 _cmd。 這兩個參數是 Runtime 消息轉發傳遞進來的。
method_setImplementation 可以讓我們提供一個新的函數來代替我們要替換的方法。 而不是將兩個方法的實現做交換。 這樣就不會造成 method_exchangeImplementations 的潛在對已有實現的副作用了。
結語
不知道大家是否注意到過 method_exchangeImplementations 所帶來的這個副作用。這種問題如果發生,調試起來會非常困難。 至少這次了解了之後, 就可以幫你減少很多潛在的隱患, 幫你節約調試問題的時間。 當然,大家如果對 Swizzle 相關的幾個方法有任何的補充,也歡迎在留言中寫出,一起分享相關知識。