Objective-C 的運行時中最具爭議的黑魔法:method swizzling境析。
Method swizzling
Method swizzling 用于改變一個已經(jīng)存在的 selector 的實現(xiàn)囚枪。這項技術(shù)使得在運行時通過改變 selector 在類的消息分發(fā)列表中的映射從而改變方法的掉用成為可能。例如:我們想要在一款 iOS app 中追蹤每一個視圖控制器被用戶呈現(xiàn)了幾次: 這可以通過在每個視圖控制器的 viewDidAppear: 方法中添加追蹤代碼來實現(xiàn)劳淆,但這樣會大量重復(fù)的樣板代碼眶拉。繼承是另一種可行的方式,但是這要求所有被繼承的視圖控制器如 UIViewController, UITableViewController, UINavigationController 都在 viewDidAppear:實現(xiàn)追蹤代碼憔儿,這同樣會造成很多重復(fù)代碼。 幸運的是放可,這里有另外一種可行的方式:從 category 實現(xiàn) method swizzling 谒臼。下面是實現(xiàn)方式:
#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);
// When swizzling a class method, use the following:
// 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
計算機科學(xué)里,交換指針指向用來交換基于名字或者位置的指針引用耀里。如果你對 Objective-C 這方面的特性不是很了解的話蜈缤,這是很值得推薦使用的一個特性,因為 method swizzling 可以通過交換 selector 來改變函數(shù)指針的引用冯挎。
現(xiàn)在底哥,UIViewController 或其子類的實例對象在調(diào)用 viewWillAppear: 的時候會有 log 的輸出。
在視圖控制器的生命周期房官,響應(yīng)事件趾徽,繪制視圖或者 Foundation 框架的網(wǎng)絡(luò)棧等方法中插入代碼都是 method swizzling 能夠為開發(fā)帶來很好作用的例子。有很多的場景選擇method swizzling 會是很合適的解決方式翰守,這顯然也會讓 Objective-C 開發(fā)者的技術(shù)變得越來越成熟孵奶。
到此我們已經(jīng)知道為什么,應(yīng)該在哪些地方使用 method swizzling蜡峰,下面介紹如何使用 method swizzling:
+load vs +initialize
swizzling應(yīng)該只在+load中完成了袁。
在 Objective-C 的運行時中朗恳,每個類有兩個方法都會自動調(diào)用。+load 是在一個類被初始裝載時調(diào)用载绿,+initialize 是在應(yīng)用第一次調(diào)用該類的類方法或?qū)嵗椒ㄇ罢{(diào)用的粥诫。兩個方法都是可選的,并且只有在方法被實現(xiàn)的情況下才會被調(diào)用崭庸。
dispatch_once
swizzling 應(yīng)該只在 dispatch_once 中完成
由于 swizzling 改變了全局的狀態(tài)怀浆,所以我們需要確保每個預(yù)防措施在運行時都是可用的。原子操作就是這樣一個用于確保代碼只會被執(zhí)行一次的預(yù)防措施冀自,就算是在不同的線程中也能確保代碼只執(zhí)行一次揉稚。Grand Central Dispatch 的 dispatch_once 滿足了所需要的需求,并且應(yīng)該被當(dāng)做使用 swizzling 的初始化單例方法的標(biāo)準(zhǔn)熬粗。
Selectors, Methods, & Implementations
在 Objective-C 的運行時中搀玖,selectors, methods, implementations 指代了不同概念,然而我們通常會說在消息發(fā)送過程中驻呐,這三個概念是可以相互轉(zhuǎn)換的灌诅。 下面是蘋果 Objective-C Runtime Reference中的描述:
- Selector(
typedef struct objc_selector *SEL
):在運行時 Selectors 用來代表一個方法的名字。Selector 是一個在運行時被注冊(或映射)的C類型字符串含末。Selector由編譯器產(chǎn)生并且在當(dāng)類被加載進內(nèi)存時由運行時自動進行名字和實現(xiàn)的映射猜拾。- Method(
typedef struct objc_method *Method
):方法是一個不透明的用來代表一個方法的定義的類型。- Implementation(
typedef id (*IMP)(id, SEL,...)
):這個數(shù)據(jù)類型指向一個方法的實現(xiàn)的最開始的地方佣盒。該方法為當(dāng)前CPU架構(gòu)使用標(biāo)準(zhǔn)的C方法調(diào)用來實現(xiàn)挎袜。該方法的第一個參數(shù)指向調(diào)用方法的自身(即內(nèi)存中類的實例對象,若是調(diào)用類方法肥惭,該指針則是指向元類對象metaclass)盯仪。第二個參數(shù)是這個方法的名字selector,該方法的真正參數(shù)緊隨其后蜜葱。
理解 selector, method, implementation 這三個概念之間關(guān)系的最好方式是:在運行時全景,類(Class)維護了一個消息分發(fā)列表來解決消息的正確發(fā)送。每一個消息列表的入口是一個方法(Method)牵囤,這個方法映射了一對鍵值對爸黄,其中鍵值是這個方法的名字 selector(SEL),值是指向這個方法實現(xiàn)的函數(shù)指針 implementation(IMP)揭鳞。 Method swizzling 修改了類的消息分發(fā)列表使得已經(jīng)存在的 selector 映射了另一個實現(xiàn) implementation炕贵,同時重命名了原生方法的實現(xiàn)為一個新的 selector。
調(diào)用 _cmd
下面代碼在正常情況下會出現(xiàn)循環(huán):
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", NSStringFromClass([self class]));
}
然而在交換了方法實現(xiàn)后就不會出現(xiàn)循環(huán)了野崇。好的程序員應(yīng)該對這里出現(xiàn)的方法的遞歸調(diào)用有所警覺鲁驶,這里我們應(yīng)該理清在 method swizzling 后方法的實現(xiàn)究竟變成了什么。在交換了方法的實現(xiàn)后舞骆,xxx_viewWillAppear:方法的實現(xiàn)已經(jīng)被替換為了 UIViewController -viewWillAppear:的原生實現(xiàn)钥弯,所以這里并不是在遞歸調(diào)用径荔。由于 xxx_viewWillAppear: 這個方法的實現(xiàn)已經(jīng)被替換為了 viewWillAppear: 的實現(xiàn),所以脆霎,當(dāng)我們在這個方法中再調(diào)用 viewWillAppear: 時便會造成遞歸循環(huán)总处。
記住給需要轉(zhuǎn)換的所有方法加個前綴以區(qū)別原生方法。
思考
很多人認(rèn)為交換方法實現(xiàn)會帶來無法預(yù)料的結(jié)果睛蛛。然而采取了以下預(yù)防措施后, method swizzling 會變得很可靠:
- 在交換方法實現(xiàn)后記得要調(diào)用原生方法的實現(xiàn)(除非你非常確定可以不用調(diào)用原生方法的實現(xiàn)):APIs 提供了輸入輸出的規(guī)則雕沿,而在輸入輸出中間的方法實現(xiàn)就是一個看不見的黑盒苞七。交換了方法實現(xiàn)并且一些回調(diào)方法不會調(diào)用原生方法的實現(xiàn)這可能會造成底層實現(xiàn)的崩潰橡庞。
- 避免沖突:為分類的方法加前綴戏仓,一定要確保調(diào)用了原生方法的所有地方不會因為你交換了方法的實現(xiàn)而出現(xiàn)意想不到的結(jié)果。
- 理解實現(xiàn)原理:只是簡單的拷貝粘貼交換方法實現(xiàn)的代碼而不去理解實現(xiàn)原理不僅會讓 App 很脆弱客冈,并且浪費了學(xué)習(xí) Objective-C 運行時的機會旭从。閱讀 Objective-C Runtime Reference 能夠讓你更好理解實現(xiàn)原理。
- 持續(xù)的預(yù)防:不管你對你理解 swlzzling 框架场仲,UIKit 或者其他內(nèi)嵌框架有多自信和悦,一定要記住所有東西在下一個發(fā)行版本都可能變得不再好使。做好準(zhǔn)備渠缕,在使用這個黑魔法中走得更遠鸽素,不要讓程序反而出現(xiàn)不可思議的行為。
文章轉(zhuǎn)自: Mattt Thompson撰寫 亦鳞, Daniel Hu翻譯