Objective-C對(duì)象收到消息之后,究竟會(huì)調(diào)用何種方法需要在運(yùn)行期才能解析出來阁将。那你也許會(huì)問:與給定的選擇子名稱相對(duì)應(yīng)的方法是不是也可以在運(yùn)行期改變呢变隔?沒錯(cuò),就是這樣茵宪。若能善用此特性最冰,則可發(fā)揮出巨大優(yōu)勢,因?yàn)槲覀兗炔恍枰创a稀火,也不需要通過繼承子類來覆寫方法就能改變這個(gè)類本身的功能暖哨。這樣一來,新功能將在本類的所有實(shí)例中生效凰狞,而不是僅限于覆寫了相關(guān)方法的那些子類實(shí)例篇裁。此方案經(jīng)常稱為“方法調(diào)配”(method swizzling)。
類的方法列表會(huì)把選擇子的名稱映射到相關(guān)的方法實(shí)現(xiàn)之上赡若,使得“動(dòng)態(tài)消息派發(fā)系統(tǒng)”能夠據(jù)此找到應(yīng)該調(diào)用的方法达布。這些方法均以函數(shù)指針的形式來表示,這種指針叫做IMP逾冬,其原型如下:
NSString類可以響應(yīng)lowercaseString黍聂、uppercaseString、capitalizedString等選擇子粉渠。這張映射表中的每個(gè)選擇子都映射到了不同的IMP之上分冈,如圖1所示圾另。
Objective-C運(yùn)行期系統(tǒng)提供的幾個(gè)方法都能夠用來操作這張表霸株。開發(fā)者可以向其中新增選擇子,也可以改變某選擇子所對(duì)應(yīng)的方法實(shí)現(xiàn)集乔,還可以交換兩個(gè)選擇子所映射到的指針去件。經(jīng)過幾次操作之后坡椒,類的方法表就會(huì)變成圖2這個(gè)樣子。
在新的映射表中尤溜,多了一個(gè)名為newSelector的選擇子倔叼,capitalizedString的實(shí)現(xiàn)也變了,而lowercaseString與uppercaseString的實(shí)現(xiàn)則互換了宫莱。上述修改均無須編寫子類丈攒,只要修改了“方法表”的布局,就會(huì)反映到程序中所有的NSString實(shí)例之上授霸。這下大家見識(shí)到此特性的強(qiáng)大之處了吧巡验?
本條將會(huì)談到如何互換兩個(gè)方法實(shí)現(xiàn)。通過此操作碘耳,可為已有方法添加新功能显设。不過在講解怎樣添加新功能之前,我們先來看看怎樣互換兩個(gè)已經(jīng)寫好的方法實(shí)現(xiàn)辛辨。想交換方法實(shí)現(xiàn)捕捂,可用下列函數(shù):
void method_exchangeImplementations(Method m1, Method m2);
此函數(shù)的兩個(gè)參數(shù)表示待交換的兩個(gè)方法實(shí)現(xiàn),而方法實(shí)現(xiàn)則可通過下列函數(shù)獲得:
Method class_getInstanceMethod(Class aClass,SEL aSelector);
此函數(shù)根據(jù)給定的選擇從類中取出與之相關(guān)的方法斗搞。執(zhí)行下列代碼指攒,即可交換前面提到的lowercaseString與uppercaseString方法實(shí)現(xiàn):
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(my_lowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
從現(xiàn)在開始,如果在NSString實(shí)例上調(diào)用lowercaseString榜旦,那么執(zhí)行的將是uppercaseString的原有實(shí)現(xiàn)幽七,反之亦然:
NSString *lowercaseString = [string lowercaseString];
NSLog(@"lowercaseString = %@",lowercaseString);
// Output: lowercaseString = STRING
NSString *uppercaseString = [string uppercaseString];
NSLog(@"uppercaseString = %@",uppercaseString);
// Output: uppercaseString = string
筆者剛才向大家演示了如何交換兩個(gè)方法實(shí)現(xiàn),然而在實(shí)際應(yīng)用中溅呢,像這樣直接交換兩個(gè)方法實(shí)現(xiàn)的澡屡,意義并不大。因?yàn)閘owercaseString與uppercaseString這兩個(gè)方法已經(jīng)各自實(shí)現(xiàn)得很好了咐旧,沒必要再交換了驶鹉。但是,可以通過這一手段來為既有的方法實(shí)現(xiàn)增添新功能铣墨。比方說室埋,想要在調(diào)用lowercaseString時(shí)記錄某些信息,這時(shí)就可以通過交換方法實(shí)現(xiàn)來達(dá)成此目標(biāo)伊约。我們新編寫一個(gè)方法姚淆,在此方法中實(shí)現(xiàn)所需的附加功能,并調(diào)用原有實(shí)現(xiàn)屡律。
新方法可以添加至NSString的一個(gè)“分類”(category)中:
@interface NSString (myAdditions)
- (NSString *)my_lowercaseString;
@end
上述新方法將與原有的lowercaseString方法互換腌逢,交換之后的方法表如圖2-5所示。
新方法的實(shí)現(xiàn)代碼可以這樣寫:
@implementation NSString (myAdditions)
- (NSString *)my_lowercaseString {
NSString *lowercase = [self my_lowercaseString];
NSLog(@" %@ ==> %@",self,lowercase);
return lowercase;
}
@end
這段代碼看上去好像會(huì)陷入遞歸調(diào)用的死循環(huán)超埋,不過大家要記住搏讶,此方法是準(zhǔn)備和lowercaseString方法互換的佳鳖。所以,在運(yùn)行期媒惕,eoc_myLowercaseString選擇子實(shí)際上對(duì)應(yīng)于原有的lowercaseString方法實(shí)現(xiàn)系吩。最后,通過下列代碼來交換這兩個(gè)方法實(shí)現(xiàn):
Method class_getInstanceMethod(Class aClass,SEL aSelector);
Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(my_lowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
執(zhí)行完上述代碼之后妒蔚,只要在NSString實(shí)例上調(diào)用lowercaseString方法穿挨,就會(huì)輸出一行記錄消息:
NSString *string = @"stRiNg";
NSString *lowercaseString = [string lowercaseString];
// Output: stRiNg ==> string
通過此方案,開發(fā)者可以為那些“完全不知道其具體實(shí)現(xiàn)的”(completely opaque肴盏,“完全不透明的”)黑盒方法增加日志記錄功能絮蒿,這非常有助于程序調(diào)試。然而叁鉴,此做法只在調(diào)試程序時(shí)有用土涝。很少有人在調(diào)試程序之外的場合用上述“方法調(diào)配技術(shù)”來永久改動(dòng)某個(gè)類的功能。不能僅僅因?yàn)镺bjective-C語言里有這個(gè)特性就一定要用它幌墓。若是濫用但壮,反而會(huì)令代碼變得不易讀懂且難于維護(hù)。