前言
OC是一門動態(tài)語言妻柒,就預(yù)示這光靠編譯過程來完全讀懂工作的方式是不夠的百姓,很多執(zhí)行語句都需要在運行時才能知道其意思楣颠,也就是runtime笛洛。
理解OC中的消息傳遞
在Object-C中夏志,函數(shù)的調(diào)用過程經(jīng)常性的使用到方法,用來傳遞消息。而消息包括名稱或選擇子(selector)沟蔑,以及參數(shù)和返回值湿诊。
oc是一門動態(tài)語言,也就說明其消息可以是動態(tài)綁定的過程瘦材,在編譯時厅须,無法決定運行時該調(diào)用的函數(shù)。
例如該語句我們沒有辦法去知道返回值的類型:
id value = [Person sendMessage:@"黃"];
整個合起來就是一個消息食棕,在其中朗和,Person為接收者(receiver),sendMessage為選擇子簿晓,后續(xù)為參數(shù)眶拉。當(dāng)編譯器收到該條消息是,通過C語言函數(shù)憔儿,也是消息傳遞機制中的核心函數(shù)objc_msgSend
進行消息傳遞忆植。
void objc_msgSend(id self, SEL cmd, ...)
該函數(shù)能接受多個參數(shù),其中第一個參數(shù)代表接收者谒臼,第二個參數(shù)是選擇子朝刊,后跟可以跟多個發(fā)送消息參數(shù)。
objc_msgSend 消息傳遞機制
objc_msgSend
會先判斷接收者對象是否存在蜈缤,如果存在情況根據(jù)接收者所屬的類中搜尋”方法列表“拾氓,如果找不到,則會向上找繼承類的”方法列表“底哥,找打找到合適就跳轉(zhuǎn)咙鞍,如果找不到則執(zhí)行消息轉(zhuǎn)發(fā)(后期會更新消息轉(zhuǎn)發(fā)機制)。
該過程雖然繁瑣叠艳,但是objc_msgSend
會為將每將匹配的結(jié)果緩存到”快速映射表“中奶陈,每個類都存在這樣的緩存易阳,后續(xù)若是需要再發(fā)送同樣的消息附较,可以直接查找映射表,節(jié)省執(zhí)行時間潦俺。
后續(xù)消息處理工作拒课,會交給一些函數(shù)來處理:
objc_msgSend_stret
如果待發(fā)送的消息要返回結(jié)構(gòu)體,可交由此函數(shù)處理事示。
objc_msgSend_fpret
如果待發(fā)送的消息要返回浮點數(shù)早像,可交由此函數(shù)處理。
objc_msgSendSuper
如果給父類發(fā)送消息肖爵,可交由此函數(shù)處理卢鹦。
前面提到消息一旦找到就會跳轉(zhuǎn)過去,之所以可以跳轉(zhuǎn)劝堪,是因為OC對象的每個方法視為簡單的C函數(shù)冀自。
void Class_selector(id self, SEL cmd, ...)
每個類都有一張表揉稚,其中的指針都會指向這種函數(shù),oc則以方法名作為key熬粗,來查表并執(zhí)行跳轉(zhuǎn)搀玖。
為了優(yōu)化跳轉(zhuǎn)方法變得簡單一些,采用”尾調(diào)用優(yōu)化“方式:如果某函數(shù)的最后一個操作是跳轉(zhuǎn)到另一個函數(shù)驻呐,編譯器會生成跳轉(zhuǎn)所需的指令碼灌诅,而不是向棧中新增新的幀,這樣減少了含末,每次調(diào)用objc_msgSend
需要新增棧幀的問題猜拾,還有減少”棧溢出“的發(fā)生。但是僅在調(diào)用其他函數(shù)時答渔,而不會把返回值另做他用的情況下关带。
根據(jù)上面的描述總結(jié)消息發(fā)送的步驟:
- 檢查接收者對象是否為nil,是則直接結(jié)束轉(zhuǎn)發(fā)沼撕,否則進入下一步宋雏;
- 查看類緩存的”快速映射表“中是否緩存該方法,該映射方法务豺,是直接發(fā)送消息磨总,否則進入下一步;
- 查看類的”方法列表“中是否存在笼沥,是緩存到映射表蚪燕,并發(fā)送消息,否則進入下一步奔浅;
- 查看父類的”快速映射表“是否緩存改方法馆纳,有就發(fā)送,否則進入下一步汹桦;
- 查看父類的"方法列表"中是否存在鲁驶,是緩存到映射表,并發(fā)送消息舞骆,否則回到第4步繼續(xù)查找更上層的父類钥弯;
- 查找到?jīng)]有父類之后,就說明并沒有該方法督禽,但是還不一定結(jié)束脆霎,后面會去進入
消息轉(zhuǎn)發(fā)機制
的步驟。
動態(tài)方法解析
上述說到狈惫,當(dāng)發(fā)送消息時睛蛛,會出現(xiàn)無法解讀消息的情況。即當(dāng)消息發(fā)送到最后,依然找不到接收消息的方式時忆肾,oc對該消息可以進行更多處理菠红,也就是動態(tài)方法解析
跟消息轉(zhuǎn)發(fā)
。
oc是動態(tài)語言难菌,在消息找不到目標(biāo)時试溯,編譯器無法解析錯誤情況并不會報錯,因為在運行時可以動態(tài)的給類添加方法郊酒,所以當(dāng)無法解析消息是遇绞,就會啟動消息轉(zhuǎn)發(fā)
機制,可由程序員手動動態(tài)處理后續(xù)的消息問題燎窘。
如果程序員沒有去處理這些問題摹闽,則會直接crach掉:
- (void)viewDidLoad {
[super viewDidLoad];
// 去調(diào)用一個沒有實現(xiàn)的方法后
[self test];
}
// 刪掉實現(xiàn)方法
//- (void)test { }
// 閃退并打印以下
*** -[ViewController test]: unrecognized selector sent to instance 0x100e01440
*** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '-[ViewController test]: unrecognized selector sent to instance 0x100e01440'
我們只調(diào)用test方法,但是沒有調(diào)用實現(xiàn)方法褐健,因此付鹿,如果沒有做異常處理的話,會直接閃退蚜迅。
消息轉(zhuǎn)發(fā)兩個階段:
第一階段是動態(tài)方法解析舵匾,先征詢接收者所屬的類,看是否能動態(tài)添加方法谁不,以處理當(dāng)>前”未知選擇子(調(diào)用的方法)“坐梯。
第二階段涉及完整的消息轉(zhuǎn)發(fā)機制,若是第一階段可以順利完成刹帕,就不會啟動消息轉(zhuǎn)發(fā)吵血。
顯然如果能越早對消息進行處理,則減少運行時所耗費的時長偷溺。
動態(tài)方法解析有兩個方法蹋辅,包括實例方法解析和類方法解析
+ (BOOL)resolveClassMethod:(SEL)sel
+ (BOOL)resolveInstanceMethod:(SEL)sel
在該例子中,如所屬類添加了resolveInstanceMethod
方法去處理挫掏,當(dāng)調(diào)用的方法沒有找到實現(xiàn)目標(biāo)時侦另,就會調(diào)用該實例方法去處理。
void dynamicMethodIMP(id self, SEL _cmd) { }
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 去調(diào)用一個沒有實現(xiàn)的方法后
[self test];
}
// 刪掉實現(xiàn)方法
//- (void)test { }
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(test)) {
class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
@end
如上圖所示砍濒,不會異常閃退淋肾,因為當(dāng)調(diào)用test方法時硫麻,找不到會直接調(diào)用resolveInstanceMethod
爸邢,而在resolveInstanceMethod
中對test,通過class_addMethod
動態(tài)添加拿愧。
備援接收者
如果resolveInstanceMethod
或者resolveClassMethod
返回YES就直接結(jié)束杠河,如果返回NO,說明動態(tài)解析沒有創(chuàng)建動態(tài)方法去處理,當(dāng)前接收者還有第二次機會能處理未知選擇子券敌,在運行時唾戚,到這一步,就會問這條消息能不能轉(zhuǎn)發(fā)給其他接收者處理待诅。
- (id)forwardingTargetForSelector:(SEL)aSelector {
Person *person = [[Person alloc]init];
if (aSelector == @selector(test)) {
return person;
} else {
return [super forwardingTargetForSelector:aSelector];
}
}
當(dāng)調(diào)用到這一步叹坦,如果返回對象,則是在告訴編譯器卑雁,把這條消息傳遞給person對象來處理募书。到這里就不會出現(xiàn)異常,如果直接返回nil或者沒有該對象测蹲,說明依然沒辦法處理結(jié)束并拋出異常莹捡。
注意:我們無法經(jīng)由這一步操作消息轉(zhuǎn)發(fā),如果需要修改消息的內(nèi)容扣甲,就要啟用完整的消息轉(zhuǎn)發(fā)
機制篮赢。
完整的消息轉(zhuǎn)發(fā)
到這一步,說明只能啟用消息轉(zhuǎn)發(fā)琉挖,通過將有關(guān)消息的全部內(nèi)容启泣,創(chuàng)建NSInvocation
對象,并封裝內(nèi)容包括選擇子示辈、目標(biāo)及參數(shù)种远。再觸發(fā)NSInvocation
對象時,通過’消息派發(fā)系統(tǒng)‘將消息指派給目標(biāo)對象顽耳。
- (void)forwardInvocation:(NSInvocation *)anInvocation
但是使用消息轉(zhuǎn)發(fā)時必須為另一個類實現(xiàn)的消息創(chuàng)建一個有效的方法簽名:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
實現(xiàn)的方式如下所致坠敷,其與第二步消息轉(zhuǎn)發(fā)等效,但是卻可以在消息觸發(fā)前射富,改變消息的內(nèi)容膝迎。比如:追加一個參數(shù)或者改變選擇子等。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if(aSelector == @selector(test))
{
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return nil;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL selector = [anInvocation selector];
// 新建需要轉(zhuǎn)發(fā)消息的對象
Cat *cat = [[Cat alloc] init];
if ([cat respondsToSelector:selector]) {
// 喚醒這個方法
[anInvocation invokeWithTarget:cat];
}
}
以上就是消息轉(zhuǎn)發(fā)的幾個步驟胰耗,每一步均有機會處理消息限次。步驟越往后,處理消息的代價就越大柴灯,如果可以在第一步就把消息處理完卖漫,運行時也可以將消息緩存起來。如果放到最后再做處理的話赠群,不僅復(fù)雜還需要創(chuàng)建和處理完整的NSInvocation對象羊始。