關於 Method Swizzling 的一點思考
經典的實現例子:
#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 複製程式碼
關於這個例子,筆者有幾點疑問:
method_exchangeImplementations
查詢資料後,給出以下回答。
為什麼可以交換?
Objc 中物件呼叫方法,被稱為訊息傳遞,其基本過程:
- 根據物件的 isa 指標,找到類。
-
在類的
objc_cache
和method_list
中,根據 method name 尋找對應方法。 - 若沒有找到,則在其父類中尋找,直到 NSObject。
- 若是在 NSObject 中沒有找到,則觸發訊息轉發機制;若找到,則跳轉到 method 中的 imp 指向的方法實現。
- 若訊息轉發機制也沒能處理,則返回 unreconized selector。
結合 runtime 程式碼(簡化後),理解上述過程。
// 訊息傳遞 id _Nullable objc_msgSend(id _Nullable self, SEL _Nonnull op, ...); // 類 struct objc_class { Class _Nonnull isaOBJC_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個好處:
- 可以在載入時就處理。
- 不用去修改原有程式碼。
為確保在不同執行緒中,處理程式碼只執行一次,需要藉助dispatch_once
。
為什麼不直接使用 method_exchangeImplementations 即可?
實現例子裡,先呼叫了class_addMethod
,再根據其結果使用class_replaceMethod
或method_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 複製程式碼
結果也在程式碼裡。
認真想想,很容易理解,處理了兩次,又交換回來了。