Objective-C Runtime (三):Method Swizzling(方法替換)

雲本尊發表於2018-06-06

Objective-C Runtime (三):Method Swizzling(方法替換)

Method Swizzling是一種改變改變一個'selector'的實際實現的技術。通過這一技術,我們可以在執行時通過修改類的分發表中selector對應的函式,來修改方法的實現。 實現圖解如下圖:

Objective-C Runtime (三):Method Swizzling(方法替換)
從上圖中,我們可以看到,使用Method Swizzling本質上是將selectorC的方法實現IMPcselectorN的方法實現IMPn交換了,當我們呼叫selectorC,也就是給物件傳送selectorC訊息時,所查詢到的對應的方法實現就是IMPn而不是IMPc了。

Method Swizzling在什麼情況下可以用到了? 例如:我們接到一個需求:對 App 的使用者行為進行追蹤和分析。簡單來說,就是,就是當使用者進入某個介面或者點選某個按鈕時,記錄這個事件。

最粗暴的方式,就是在每個 viewDidAppear 裡新增記錄事件的程式碼。這種方式缺點是很明顯的,它破壞了程式碼的乾淨整潔。因為記錄事件的程式碼本身不屬於原有程式碼的主要邏輯。隨著專案擴大、程式碼增加,我們的原有程式碼裡會到處分佈著記錄事件的程式碼。這時,要找到一段事件記錄的程式碼會變得困難,也很容易忘記新增事件記錄的程式碼。

我們可能會想到使用繼承或類別,在重寫的方法裡新增事件記錄的程式碼。但這樣也會帶來新的問題:

  1. 我們無法控制別人如何去例項化我們的子類;
  2. 對於類別,我們沒辦法呼叫到原來的方法實現。大多時候,我們重寫一個方法只是為了新增一些程式碼,而不是完全取代它;
  3. 如果有兩個類別都實現了相同的方法,執行時沒法保證哪一個類別的方法會給呼叫;
  4. 每個 ViewController 裡的 ButtonClick 方法命名不可能都一樣。

我瞭解決以上的問題,我們可以使用Method Swizzling,如以下程式碼所示:

#import "UIViewController+Tracking.h"
#import <objc/runtime.h>
@implementation UIViewController (Tracking)


+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(track_viewWillAppear:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        BOOL didAddMethod =
        class_addMethod(class,
                        originalSelector,
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
    
}


- (void)track_viewWillAppear:(BOOL)animated {
    [self track_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}

@end
複製程式碼

從上面程式碼可以看出,我們通過method swizzling修改了UIViewController@selector(viewWillAppear:)對應的函式指標,使其實現指向了我們自定義的track_viewWillAppear:的實現。這樣,當UIViewController及其子類的物件呼叫viewWillAppear時,都會列印一條日誌資訊。

上面程式碼需要解釋的問題: class_addMethod:要先嚐試新增原 selector 是為了做一層保護,因為如果這個類沒有實現 originalSelector ,但其父類實現了,那 class_getInstanceMethod 會返回父類的方法。這樣 method_exchangeImplementations 替換的是父類的那個方法,這當然不是你想要的。所以我們先嚐試新增 orginalSelector ,如果已經存在,再用 method_exchangeImplementations 把原方法的實現跟新的方法實現給交換掉。

注意事項 Swizzling通常被稱作是一種黑魔法,容易產生不可預知的行為和無法預見的後果。濫用可能會造成很多問題,如果遵從以下幾點預防措施的話,還是比較安全的:

  1. Swizzling應該總是在+load中執行;
  2. Swizzling應該總是在dispatch_once中執行;
  3. 總是呼叫方法的原始實現(除非有更好的理由不這麼做):API提供了一個輸入與輸出約定,但其內部實現是一個黑盒。Swizzle一個方法而不呼叫原始實現可能會打破私有狀態底層操作,從而影響到程式的其它部分;
  4. 避免衝突:給自定義的分類方法加字首,從而使其與所依賴的程式碼庫不會存在命名衝突。

參考:

  1. nshipster.com/method-swiz…

相關文章