看到nshipster 的Method Swizzling這篇不錯的文章還沒翻譯,就補充一下,沒有逐字翻譯,關于associated objects已經有翻譯了,大家也可以去了解一下。
??method swizzling也許是runtime中最有爭議的技術,它的作用就是改變已經存在selector的實現(xiàn),之所以可以這樣是因為方法調用可以在運行時改變:通過改變類的分發(fā)表( dispatch table,該表包含selector的名稱及對應實現(xiàn)函數(shù)的地址)里selector和實現(xiàn)之間的對應關系。
??舉個例子,比如你想記錄一個iOS應用里每個view controller顯示的次數(shù):可以在每個view controller添加記錄的代碼,但這會導致大量的重復代碼;通過繼承也是一個方法,但需要同時創(chuàng)建UIViewController, UITableViewController, UINavigationController及其它中view controller的子類,同樣也會產生許多重復的代碼出現(xiàn)。
??幸運的是,在UIViewController的category使用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);
// 如果 swizzling 的是類方法, 采用如下的方式:
// Class class = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(class, originalSelector);
// Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
//交換實現(xiàn)
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}
@end
現(xiàn)在,當一個UIViewController或者其子類的實例調用viewWillAppear:方法時,就會打印出一條記錄。假如要在在view controller的生命周期,view的繪制或者Foundation的網絡協(xié)議棧注入一些自定的行為,method swizzling也許是你應該考慮的一個方向。
下面是使用method swizzling應該注意的點:
+load vs. +initialize
Swizzling應該只在load方法中使用
oc會在運行時自動調用每個類的兩個方法,+load 會在類初始化加載的時候調用;+initialize方法會在程序調用類的第一個實例或者類方法的時候調用。這兩個方法都是可選的,只會在實現(xiàn)的時候才去調用。由于method swizzling會影響到全局的狀態(tài),因此最小化競爭條件的出現(xiàn)變得很重要,+load方法能夠確保在類的初始化時候調用,這能夠保證改變應用行為的一致性,而+initialize在執(zhí)行時并不提供這種保證,實際上,如果沒有直接給這個類發(fā)送消息,該方法可能都不會調用到。
dispatch_once
Swizzling應該只在dispatch_once中完成
如上,由于swizzling會改變全局狀態(tài),所以我們需要在運行時采取一些預防措施。原子性就是其中的一種預防措施,因為它能保證不管有多少個線程,代碼只會執(zhí)行一次。GCD的dispatch_once 能夠滿足這種需求,因此在method swizzling應該將其作為最佳的實踐方式。
選擇器,方法和實現(xiàn)
在oc中,選擇器、方法和實現(xiàn)是運行時的特殊方面,雖然在一般情況下,這些術語是用在消息發(fā)送的過程中。
下面是Apple對它們的幾個描述:
- 選擇器(Selector-typedef struct objc_selector *SEL ):用于在運行時表示一個方法的名稱,一個方法選擇器就是一個C字符串,在運行時會被注冊或者映射,選擇器是由編譯器生成的,并在類被加載的時候由運行時自動進行映射。
- 方法(Method-typedef struct objc_method *Method):在類的定義中代表一個方法的類型。
- 實現(xiàn)(Implementation- typedef id (*IMP)(id, SEL, ...)):這是一個指向方法實現(xiàn)函數(shù)起始地址的指針,這個函數(shù)的第一個參數(shù)是指向self的指針,第二個參數(shù)是方法選擇器,然后是方法的參數(shù)。
理解它們之間關系的最好方式是:一個類維護著一張分發(fā)表(dispatch table),用來解析運行時發(fā)來的消息;該表的每個入口是一個方法(Method),其中的key就是選擇器(SEL),對應一個實現(xiàn)(IMP),即一個指向底層c函數(shù)的指針。
swizzle 一個方法就是改變類的分發(fā)表,使它在解析消息的時候,將一個選擇器selector對應到別的實現(xiàn),并且將該選擇器對應的原始實現(xiàn)關聯(lián)到新的選擇器中。
調用 _cmd
看起來下面的代碼可能導致無限循環(huán):
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", NSStringFromClass([self class]));
}
可奇怪的是,它并不會。在swizzling的過程中,xxx_viewWillAppear:已經被重新指向UIViewController 的原始實現(xiàn)-viewWillAppear:,但是如果我們在這個方法中調用viewWillAppear:則會導致無限循環(huán)。
注意事項
通常認為Swizzling是一個比較危險的技術,容易產生不可預料的行為和無法預見的后果,但只要遵循以下幾個注意事項,其實method swizzlin還是相對安全的。
- 總是調用一個方法的原始實現(xiàn)(除非你有足夠好的理由不這么做):API提供了輸入和輸出的約定,但其中的實現(xiàn)卻是黑盒。Swizzling 一個方法但不去調用其原始實現(xiàn),可能造成私有狀態(tài)的底層假設被打破,影響程序的其它部分。
- 避免沖突: 給category方法加前綴,確保不會跟其它依賴的代碼產生沖突。
- 知道到底發(fā)生啥了:簡單的復制粘貼swizzling 代碼,而不清楚其如何工作不僅非常危險,而且浪費了好多深入學習objective-c運行時的機會,可以通過查看 Objective-C Runtime Reference 和<objc/runtime.h>頭文件了解其中的一些來龍去脈。
- 小心的處理:不管你在swizzling Foundation、UIKit或者其它內建framework方法時多么的充滿自信,必須清楚在下一個版本這些可能都改變了,所以為了不出差錯,還是需要多花點心思的。