您將了解到了runtime
是如何通過objc_msgSend
在運(yùn)行時(shí)把方法和方法實(shí)現(xiàn)進(jìn)行動(dòng)態(tài)綁定的忆矛;
也將了解到runtime下動(dòng)態(tài)方法解析和消息轉(zhuǎn)發(fā)的機(jī)制是怎樣的健提。
消息
本章描述了代碼的消息表達(dá)式如何轉(zhuǎn)換為對(duì)objc_msgSend
函數(shù)的調(diào)用砚嘴,如何通過名字來指定一個(gè)方法聪铺,以及如何使用objc_msgSend
函數(shù)亥揖。
獲得方法地址
避免動(dòng)態(tài)綁定的唯一辦法就是取得方法的地址妙同,并且直接象函數(shù)調(diào)用一樣調(diào)用它。
當(dāng)一個(gè)方法會(huì)被連續(xù)調(diào)用很多次矢门,而且您希望節(jié)省每次調(diào)用方法都要發(fā)送消息的開銷時(shí)盆色,使用方法地址來調(diào)用方法就顯得很有效。
利用NSObject類中的methodForSelector:
方法祟剔,您可以獲得一個(gè)指向方法實(shí)現(xiàn)的指針隔躲,并可以使用該指針直接調(diào)用方法實(shí)現(xiàn)。methodForSelector:
返回的指針和賦值的變量類型必須完全一致物延,包括方法的參數(shù)類型和返回值類型都在類型識(shí)別的考慮范圍中宣旱。
下面的例子展示了怎么使用指針來調(diào)用setFilled:
的方法實(shí)現(xiàn):
void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)];
for ( i = 0; i < 1000, i++ )
setter(targetList[i], @selector(setFilled:), YES);
方法指針的第一個(gè)參數(shù)是接收消息的對(duì)象(self
),第二個(gè)參數(shù)是方法選標(biāo)(_cmd
)教届。這兩個(gè)參數(shù)在方法中是隱藏參數(shù)响鹃,但使用函數(shù)的形式來調(diào)用方法時(shí)必須顯示的給出。
使用methodForSelector:
來避免動(dòng)態(tài)綁定將減少大部分消息的開銷案训,但是這只有在指定的消息被重復(fù)發(fā)送很多次時(shí)才有意義买置,例如上面的 for 循環(huán)。
注意强霎,methodForSelector:
是 Cocoa 運(yùn)行時(shí)系統(tǒng)的提供的功能忿项,而不是 Objective-C 語言本身的功 能。
objc_msgSend
在objective-C中城舞,消息時(shí)知道運(yùn)行時(shí)才會(huì)與方法實(shí)現(xiàn)進(jìn)行綁定的轩触。編譯器會(huì)把一個(gè)消息表達(dá)式:
[receiver message]
轉(zhuǎn)換成一個(gè)對(duì)消息函數(shù)objc_msgSend
的調(diào)用。該函數(shù)有兩個(gè)主要參數(shù):消息接收者和消息對(duì)應(yīng)的方法名字---即方法選標(biāo)家夺。
objc_msgSend(receive, selector)
同時(shí)接收消息中的任意數(shù)目的參數(shù):
objc_msgSend(receive, select, arg1, arg2, ...)
該消息函數(shù)做了動(dòng)態(tài)綁定所需要的一切:
它首先找到選標(biāo)所對(duì)應(yīng)的方法實(shí)線脱柱。因?yàn)椴煌念悓?duì)同一方法可能會(huì)有不同的實(shí)現(xiàn),所以找到的方法實(shí)線依賴于消息接收者的類型拉馋。
然后將消息接受者對(duì)象(指向消息接受者對(duì)象的指針)以及方法中指定的參數(shù)傳給找到的方法實(shí)現(xiàn)榨为。
最后,將方法實(shí)現(xiàn)的返回值作為該函數(shù)的返回值返回煌茴。
注意:objc_msgSend方法看起來好像返回了數(shù)據(jù)随闺,其實(shí)objc_msgSend從不返回?cái)?shù)據(jù),而是你的方法在運(yùn)行時(shí)方法實(shí)現(xiàn)被調(diào)用后才會(huì)返回?cái)?shù)據(jù)蔓腐。下面詳細(xì)敘述消息發(fā)送的步驟(如下圖):
消息框架:
這樣就能解釋objc_msgSend
工作原理了矩乐,當(dāng)對(duì)象收到消息時(shí),為了匹配消息的接收者和選擇子:
- 消息函數(shù)首先根據(jù)對(duì)象的
isa
指針找到該對(duì)象所對(duì)應(yīng)的類的方法列表objc_method_list
回论,并從方法列表中尋找該消息對(duì)應(yīng)的方法選標(biāo)散罕。如果能找到就可以直接跳轉(zhuǎn)到相關(guān)的具體實(shí)現(xiàn)中去調(diào)用。 - 如果找不到傀蓉,將會(huì)通過
super_class
指針沿著繼承樹向上去搜索笨使,直到繼承樹根部(通常為NSObject類)。一旦找到了方法選標(biāo)僚害,objc_msgSend
則以消息接收者對(duì)象為參數(shù)調(diào)用硫椰,調(diào)用該選標(biāo)對(duì)應(yīng)的方法實(shí)現(xiàn)。 - 如果到了繼承樹根部還沒有找到萨蚕,就會(huì)進(jìn)行消息轉(zhuǎn)發(fā)靶草,還有三次機(jī)會(huì)來處理。(消息轉(zhuǎn)發(fā)在下文有介紹)
這就是在運(yùn)行時(shí)系統(tǒng)中選擇方法實(shí)現(xiàn)的方式岳遥。在面向?qū)ο缶幊讨修认瑁话惴Q作方法和消息動(dòng)態(tài)綁定的過程。
為了加快消息的處理過程浩蓉,運(yùn)行時(shí)系統(tǒng)通常會(huì)將使用過的方法選標(biāo)和方法實(shí)現(xiàn)的地址放入緩存中派继。每個(gè)類
都有一個(gè)獨(dú)立的緩存宾袜,同時(shí)包括繼承的方法和在該類中定義的方法。消息函數(shù)會(huì)首先檢查消息接收者對(duì)象
對(duì)應(yīng)的類的緩存(理論上驾窟,如果一個(gè)方法被使用過一次庆猫,那么它很可能被再次使用)。如果在緩存中已經(jīng)
有了需要的方法選標(biāo)绅络,則消息僅僅比函數(shù)調(diào)用慢一點(diǎn)點(diǎn)月培。如果程序運(yùn)行了足夠長(zhǎng)的時(shí)間,幾乎每個(gè)消息都
能在緩存中找到方法實(shí)現(xiàn)恩急。程序運(yùn)行時(shí)杉畜,緩存也將隨著新的消息的增加而增加。
使用隱藏的參數(shù)
疑問:
我們經(jīng)常用到關(guān)鍵字self
衷恭,但是self
是如何獲取當(dāng)前方法的對(duì)象呢此叠?
其實(shí),這也是runtime
系統(tǒng)的作用随珠,self
是在方法運(yùn)行時(shí)被動(dòng)態(tài)傳入的拌蜘。
當(dāng)objc_msgSend
找到方法對(duì)應(yīng)實(shí)現(xiàn)時(shí),它將直接調(diào)用該方法實(shí)現(xiàn)牙丽,并將消息中所有參數(shù)都傳遞給方法實(shí)現(xiàn)简卧,同時(shí),她還將傳遞兩個(gè)隱藏參數(shù):
- 接收消息的對(duì)象(
self
所指向的內(nèi)容烤芦,當(dāng)前方法的對(duì)象指針) - 方法選擇器(
_cmd
指向的內(nèi)容举娩,當(dāng)前方法的SEL
指針)
這些參數(shù)幫助方法實(shí)現(xiàn)獲得了消息表達(dá)式的信息。它們被認(rèn)為是“隱藏”的是因?yàn)樗鼈儾]有在定義方法的源代碼中聲明构罗,而是在代碼編譯時(shí)插入方法實(shí)現(xiàn)中的铜涉。盡管這些參數(shù)沒有被明確聲明,在源代碼中我們?nèi)匀豢梢砸盟鼈儭?/p>
這兩個(gè)參數(shù)中遂唧,self
更實(shí)用芙代。它是在方法實(shí)現(xiàn)中訪問消息接收者對(duì)象的實(shí)例變量的途徑。
這時(shí)我們可能會(huì)想到另一個(gè)關(guān)鍵字super
盖彭,實(shí)際上super
關(guān)鍵字接收到消息時(shí)纹烹,編譯器會(huì)創(chuàng)建一個(gè)objc_super
結(jié)構(gòu)體:
struct objc_super { id receiver; Class class;}
這個(gè)結(jié)構(gòu)體指明了消息應(yīng)該被傳遞給特定的父類。receiver
仍然是self
本身召边,當(dāng)我們想通過[super class]
獲取父類時(shí)铺呵,編譯器其實(shí)是將指向self
的id
指針和class
的SEL
傳遞給objc_msgSendSuper
函數(shù)。只有在NSObject類中才能找到class
方法隧熙,然后class
方法底層被轉(zhuǎn)換為object_getClass()
片挂,接著底層編譯器將代碼轉(zhuǎn)換為objc_msgSend(objc_super->receiver, @selector(class))
,傳入的第一個(gè)參數(shù)是指向self
的id
指針,與調(diào)用[self class]
相同音念,所以我們得到的永遠(yuǎn)都是self
的類型沪饺。因此你會(huì)發(fā)現(xiàn):
// 這句話并不能獲取父類的類型,只能獲取當(dāng)前類的類型名
NSLog(@"%@", NSStringFromClass([super class]));
消息轉(zhuǎn)發(fā)
消息轉(zhuǎn)發(fā)機(jī)制基本分為三個(gè)步驟:
1闷愤、動(dòng)態(tài)方法解析
2整葡、備用接收者
3、完整轉(zhuǎn)發(fā)
整個(gè)消息轉(zhuǎn)發(fā)流程如下圖所示:
1肝谭、所屬類動(dòng)態(tài)方法解析
首先掘宪,如果沿著繼承樹沒有搜索到相關(guān)方法則會(huì)向接受者所屬的類進(jìn)行一次請(qǐng)求蛾扇,調(diào)用所屬類的類方法 +resolveInstanceMethod:(實(shí)例方法)
或者 +resolveClassMethod:(類方法)
攘烛。在這個(gè)方法中,我們有機(jī)會(huì)為該未知消息新增一個(gè)”處理方法“镀首。不過使用該方法的前提是我們已經(jīng)實(shí)現(xiàn)了該”處理方法”坟漱,只需要在運(yùn)行時(shí)通過class_addMethod
函數(shù)動(dòng)態(tài)添加到類里面就可以了。
+ (BOOL)resolveInstanceMethod:(SEL)sel;
+ (BOOL)resolveClassMethod:(SEL)sel;
舉個(gè)例子:
// Person.m
#import "Person.h"
#import <objc/runtime.h>
@interface Person()
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Person
- (instancetype)init {
if (self = [super init]) {
}
return self;
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"resolveInstanceMethod: %@", NSStringFromSelector(sel));
if (sel == @selector(appendString:)) {
class_addMethod([self class], sel, (IMP)dynamicAdditonMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
+ (BOOL)resolveClassMethod:(SEL)sel {
NSLog(@"resolveClassMethod: %@", NSStringFromSelector(sel));
return [super resolveClassMethod:sel];
}
void dynamicAdditonMethodIMP(id self, SEL _cmd) {
NSLog(@"dynamicAdditonMethodIMP");
}
@end
// viewController.m
id *p = [[Person alloc] init];
[p appendString:@""];
輸出結(jié)果:
2018-06-12 14:38:54.461050+0800 getIP[12036:607027] resolveInstanceMethod: appendString:
2018-06-12 14:38:54.461230+0800 getIP[12036:607027] dynamicAdditonMethodIMP
首先創(chuàng)建了一個(gè)Person的實(shí)例對(duì)象更哄,一定要用id
類型來聲明芋齿,否則會(huì)在編譯器就報(bào)錯(cuò),因?yàn)檎也坏较嚓P(guān)函數(shù)的聲明(這里是appendString:)成翩。id
類型由于可以指向任何類型的對(duì)象觅捆,因此編譯時(shí)能夠找到NSString類的相關(guān)方法聲明就不會(huì)報(bào)錯(cuò)。
由于Person類沒有聲明和定義appendString:
方法麻敌,所以運(yùn)行時(shí)應(yīng)該會(huì)報(bào)unrecognized selector
錯(cuò)誤栅炒,但是并沒有,因?yàn)槲覀冎貙懥祟惙椒?code>+ (BOOL)resolveInstanceMethod:(SEL)sel术羔,當(dāng)找不到相關(guān)實(shí)例方法的時(shí)候就會(huì)調(diào)用該類方法去詢問是否可以動(dòng)態(tài)添加赢赊,如果返回YES
就會(huì)再次執(zhí)行相關(guān)方法,如何給一個(gè)類動(dòng)態(tài)添加一個(gè)方法级历,那就是調(diào)用runtime
庫中的class_addMethod
方法释移,該方法原型是:
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
第一個(gè)參數(shù)是需要添加方法的類;
第二個(gè)參數(shù)是一個(gè)selector
寥殖,也就是實(shí)例方法的名字玩讳;
第三個(gè)參數(shù)是一個(gè)IMP
類型的變量也是函數(shù)實(shí)現(xiàn),需要傳入一個(gè)C函數(shù)嚼贡,這個(gè)函數(shù)至少兩個(gè)參數(shù)锋边,一個(gè)是id self
一個(gè)是SEL _cmd
;
第四個(gè)參數(shù)是函數(shù)類型编曼,更多含義見:Type Encodings豆巨;
2、備用接收者
動(dòng)態(tài)方法解析無法處理消息時(shí)掐场,則會(huì)走備用接收者往扔。這個(gè)備用接收者只能是一個(gè)新的對(duì)象贩猎,不能是self
本身,否則就會(huì)出現(xiàn)無線循環(huán)萍膛。如果我們沒有指定相應(yīng)的對(duì)象來處理aSelector吭服,則應(yīng)該調(diào)用父類的實(shí)現(xiàn)來返回結(jié)果。
Person類聲明兩個(gè)方法:
@interface Person : NSObject
- (void)hello;
+ (Person *)hi;
@end
實(shí)現(xiàn)在Person.m中實(shí)現(xiàn)新的接收對(duì)象_helper和forwardingTargetForSelector:方法:
@interface Person()
{
RuntimeMethodHelper *_helper;
}
@end
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"forwardingTargetForSelector");
NSString *selectorString = NSStringFromSelector(aSelector);
// 將消息交給_helper來處理
if ([selectorString isEqualToString:@"hello"]) {
return _helper;
}
return [super forwardingTargetForSelector:aSelector];
}
RuntimeMethodHelper類需要實(shí)現(xiàn)轉(zhuǎn)發(fā)的方法:
#import "RuntimeMethodHelper.h"
@implementation RuntimeMethodHelper
- (void)hello {
NSLog(@"%@, %p", self, _cmd);
}
@end
最后在viewController.m中調(diào)用:
id p = [[Person alloc] init];
[p hello];
輸出結(jié)果:
2018-06-12 16:54:26.113808+0800 getIP[13842:768645] forwardingTargetForSelector
2018-06-12 16:54:26.114031+0800 getIP[13842:768645] <RuntimeMethodHelper: 0x60400000e270>, 0x10ed5f93b
3蝗罗、消息重定向
如果動(dòng)態(tài)方法解析和備用接收者都沒有處理這個(gè)消息艇棕,就只剩最后一次機(jī)會(huì),那就是消息重定向串塑。這個(gè)時(shí)候runtime
會(huì)將未知消息的所有細(xì)節(jié)都封裝為NSInvocation
對(duì)象沼琉,然后調(diào)用下述方法:
- (void)forwardInvocation:(NSInvocation *)anInvocation;
forwardInvocation:
消息給這個(gè)問題提供了一個(gè)更特別的,動(dòng)態(tài)的解決方案:當(dāng)一個(gè)對(duì)象由于沒有相應(yīng)的方法實(shí)現(xiàn)而無法響應(yīng)某消息時(shí)桩匪,運(yùn)行時(shí)系統(tǒng)將通過forwardInvocation:
消息通知該對(duì)象打瘪。每個(gè)對(duì)象都從 NSObject類中繼承了forwardInvocation:
方法。然而傻昙,NSObject 中的方法實(shí)現(xiàn)只是簡(jiǎn)單地調(diào)用了 doesNotRecognizeSelector:
闺骚。通過實(shí)現(xiàn)您自己的forwardInvocation:
方法,您可以在該方法實(shí)現(xiàn)中將消息轉(zhuǎn)發(fā)給其它對(duì)象妆档。
要轉(zhuǎn)發(fā)消息給其他對(duì)象時(shí)僻爽,forwardInvocation:
方法所必須做的有:
- 決定將消息轉(zhuǎn)發(fā)給誰
- 并且,將消息和原來的參數(shù)一塊轉(zhuǎn)發(fā)出去
注意:forward意思是“轉(zhuǎn)寄”贾惦,forwardingTargetForSelector:和forwardInvocation:都是把消息轉(zhuǎn)發(fā)給一個(gè)新的接收對(duì)象胸梆。
這里消息可以通過invokeWithTarget:
方法來轉(zhuǎn)發(fā):
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"forwardInvocation");
if ([RuntimeMethodHelper instancesRespondToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:_helper];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
if ([RuntimeMethodHelper instancesRespondToSelector:aSelector]) {
signature = [RuntimeMethodHelper instanceMethodSignatureForSelector:aSelector];
}
}
return signature;
}
運(yùn)行結(jié)果:
2018-06-12 17:21:22.255362+0800 getIP[14454:801341] forwardInvocation
2018-06-12 17:21:22.255588+0800 getIP[14454:801341] <RuntimeMethodHelper: 0x604000016fd0>, 0x10c80f8e9
轉(zhuǎn)發(fā)消息后的返回值將返回給原來的消息發(fā)送者。你可以返回任何類型的返回值纤虽,包括id乳绕,結(jié)構(gòu)體,浮點(diǎn)數(shù)等逼纸。
總結(jié)
至此洋措,我們了解到了runtime
是如何通過objc_msgSend
在運(yùn)行時(shí)把方法和方法實(shí)現(xiàn)進(jìn)行動(dòng)態(tài)綁定的;也了解到如果沿繼承樹找不到IMP
杰刽,如何進(jìn)行動(dòng)態(tài)方法解析和消息轉(zhuǎn)發(fā)的菠发。