你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> ObjC黑科技

ObjC黑科技

編輯:IOS開發基礎

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

命令行中還輸出了 。 這個是我們剛剛加入的 NSLog 在起作用。 為什麼這時候的 self 變成了 SKPayment 呢?

這就是 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 相關的幾個方法有任何的補充,也歡迎在留言中寫出,一起分享相關知識。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved