關於 Method Swizzling 的一點思考

JiandanDream發表於2019-02-13

原文連結

寫在前面

經典的實現例子:

#import <objc/runtime.h>

@implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        // 如果交換的是類方法,則使用以下程式碼:
        // Class class = object_getClass((id)self);
        // ...
        // Method originalMethod = class_getClassMethod(class, originalSelector);
        // Method swizzledMethod = class_getClassMethod(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);
        }
    });
}

#pragma mark - Method Swizzling

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

@end
複製程式碼

關於這個例子,筆者有幾點疑問:

  1. 為什麼可以交換?
  2. 為什麼要在 load 方法裡處理?
  3. 為什麼不直接使用 method_exchangeImplementations 即可?

查詢資料後,給出以下回答。

為什麼可以交換?

Objc 中物件呼叫方法,被稱為訊息傳遞,其基本過程:

  1. 根據物件的 isa 指標,找到類。
  2. 在類的 objc_cachemethod_list 中,根據 method name 尋找對應方法。
  3. 若沒有找到,則在其父類中尋找,直到 NSObject。
  4. 若是在 NSObject 中沒有找到,則觸發訊息轉發機制;若找到,則跳轉到 method 中的 imp 指向的方法實現。
  5. 若訊息轉發機制也沒能處理,則返回 unreconized selector。

結合 runtime 程式碼(簡化後),理解上述過程。

// 訊息傳遞
id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...);

// 類
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    struct objc_method_list * _Nullable * _Nullable methodLists;
    struct objc_cache * _Nonnull cache;
}

// 方法
struct method_t {
    SEL name;
    const char *types;
    IMP imp;
}

// IMP 的宣告
id (*IMP)(id, SEL, ...)
複製程式碼

可以看出,要想修改方法的實現,只需要修改 imp,因為它指向了方法的實現。

又得益於 Objc 的 Runtime System,在執行期,可以向類中新增或替換特定方法實現。

所以在 Objc 中實現方法交換,並不是一件很難的事。

為什麼要在 load 方法裡處理?

initialize 和 load 方法,都會自動呼叫,所以交換方法,可在二者選其一。

官方文件裡對 load 的解釋:

Invoked whenever a class or category is added to the Objective-C runtime; implement this method to perform class-specific behavior upon loading.

即當類或分類被載入時,load 方法就會被呼叫。

而對於 Initialize:

Initializes the class before it receives its first message.

即在類被髮送第一條訊息時,runtime system 會對該類傳送一個 initialize() 的訊息。

顯然,若是在分類中的 load 實現方法交換,有2個好處:

  1. 可以在載入時就處理。
  2. 不用去修改原有程式碼。

為確保在不同執行緒中,處理程式碼只執行一次,需要藉助 dispatch_once

為什麼不直接使用 method_exchangeImplementations 即可?

實現例子裡,先呼叫了 class_addMethod,再根據其結果使用 class_replaceMethodmethod_exchangeImplementations

為何多此一舉,不直接使用 method_exchangeImplementations 呢?

原因是被交換的方法,有可能沒在本類中實現,而是在其父類中實現,此時,就需要將其加入到本類中。

所以才有了這樣的程式碼:

// 新增 originalSelector 對應的方法
// 注意程式碼實現的效果是:originalSelector -> swizzledMethod
// 若是方法已經存在,則 didAddMethod 為 NO
BOOL didAddMethod = class_addMethod(class,
    originalSelector,
    method_getImplementation(swizzledMethod),
    method_getTypeEncoding(swizzledMethod));

if (didAddMethod) {
    // originalMethod 在上面新增成功了
    // 下面程式碼實現: swizzledSelector -> originalMethod
class_replaceMethod(class,
    swizzledSelector,
    method_getImplementation(originalMethod),
    method_getTypeEncoding(originalMethod));
} else {
    // 方法已經存在,直接交換
    method_exchangeImplementations(originalMethod, swizzledMethod);
}
複製程式碼

思考題

假設有兩個分類,都在 load 裡,進行了同樣的方法交換,那麼再呼叫原來的方法,結果會是如何呢?

以下為簡單的程式碼例子:

@implementation Cat
- (void)run {
    NSLog(@"ori");
}
@end

@impelmentation Cat (A)
// load 裡將 run 交換成 a_run

- (void)a_run {
    [self a_run];
    NSLog(@"A");
}
@end

@impelmentation Cat (B)
// load 裡將 run 交換成 a_run

- (void)a_run {
    [self a_run];
    NSLog(@"B");
}
@end

// 執行以下程式碼,會得到什麼結果呢?
[cat run]; // result: ori
複製程式碼

結果也在程式碼裡。

認真想想,很容易理解,處理了兩次,又交換回來了。

參考資料

iOS開發·runtime原理與實踐: 訊息轉發篇(Message Forwarding) (訊息機制,方法未實現+API不相容奔潰,模擬多繼承) - 掘金

Method Swizzling - NSHipster

load() - NSObject | Apple Developer Documentation

initialize() - NSObject | Apple Developer Documentation

相關文章