寫在前面
經典的實現例子:
#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
複製程式碼
關於這個例子,筆者有幾點疑問:
- 為什麼可以交換?
- 為什麼要在 load 方法裡處理?
- 為什麼不直接使用
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 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個好處:
- 可以在載入時就處理。
- 不用去修改原有程式碼。
為確保在不同執行緒中,處理程式碼只執行一次,需要藉助 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
複製程式碼
結果也在程式碼裡。
認真想想,很容易理解,處理了兩次,又交換回來了。
參考資料
iOS開發·runtime原理與實踐: 訊息轉發篇(Message Forwarding) (訊息機制,方法未實現+API不相容奔潰,模擬多繼承) - 掘金