Objective-C Runtime (三):Method Swizzling(方法替換)
Method Swizzling
是一種改變改變一個'selector'的實際實現的技術。通過這一技術,我們可以在執行時通過修改類的分發表中selector對應的函式,來修改方法的實現。
實現圖解如下圖:
Method Swizzling
本質上是將selectorC
的方法實現IMPc
與selectorN
的方法實現IMPn
交換了,當我們呼叫selectorC
,也就是給物件傳送selectorC
訊息時,所查詢到的對應的方法實現就是IMPn
而不是IMPc
了。
那Method Swizzling
在什麼情況下可以用到了?
例如:我們接到一個需求:對 App 的使用者行為進行追蹤和分析。簡單來說,就是,就是當使用者進入某個介面或者點選某個按鈕時,記錄這個事件。
最粗暴的方式,就是在每個 viewDidAppear
裡新增記錄事件的程式碼。這種方式缺點是很明顯的,它破壞了程式碼的乾淨整潔。因為記錄事件
的程式碼本身不屬於原有程式碼的主要邏輯。隨著專案擴大、程式碼增加,我們的原有程式碼裡會到處分佈著記錄事件
的程式碼。這時,要找到一段事件記錄的程式碼會變得困難,也很容易忘記新增事件記錄的程式碼。
我們可能會想到使用繼承或類別,在重寫的方法裡新增事件記錄的程式碼。但這樣也會帶來新的問題:
- 我們無法控制別人如何去例項化我們的子類;
- 對於類別,我們沒辦法呼叫到原來的方法實現。大多時候,我們重寫一個方法只是為了新增一些程式碼,而不是完全取代它;
- 如果有兩個類別都實現了相同的方法,執行時沒法保證哪一個類別的方法會給呼叫;
- 每個 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通常被稱作是一種黑魔法,容易產生不可預知的行為和無法預見的後果。濫用可能會造成很多問題,如果遵從以下幾點預防措施的話,還是比較安全的:
- Swizzling應該總是在+load中執行;
- Swizzling應該總是在dispatch_once中執行;
- 總是呼叫方法的原始實現(除非有更好的理由不這麼做):API提供了一個輸入與輸出約定,但其內部實現是一個黑盒。Swizzle一個方法而不呼叫原始實現可能會打破私有狀態底層操作,從而影響到程式的其它部分;
- 避免衝突:給自定義的分類方法加字首,從而使其與所依賴的程式碼庫不會存在命名衝突。
參考: