之前寫了篇關(guān)于消息發(fā)送的文章,其中提及到了消息轉(zhuǎn)發(fā)运褪,由于 OC 是一門動態(tài)的語言惊楼,在編譯時,編譯器還無法確定到底有沒有這個方法秸讹,因為在運行時我們還可以動態(tài)的給它添加對應(yīng)的方法處理檀咙,我曾說過,當(dāng)一個方法在自己或其繼承結(jié)構(gòu)上面的方法列表中都沒有找到璃诀,就會啟動對應(yīng)的 "消息轉(zhuǎn)發(fā)" 機制弧可。
好,現(xiàn)在我們開始好好說說 "消息轉(zhuǎn)發(fā)" 究竟是怎么一回事劣欢∽厮校總得來說,消息轉(zhuǎn)發(fā)的流程分為兩大階段氧秘。先來說第一階段年鸳,在第一階段時,當(dāng)接受者無法處理發(fā)送的消息時丸相,首先會觸發(fā)當(dāng)前類的 resolveInstanceMethod: 或者對應(yīng)的 resolveClassMethod: 方法搔确,在這個方法中會詢問接受者,是否能夠動態(tài)的給它添加對應(yīng)的方法來處理這個未知的消息灭忠,這個行為的專業(yè)術(shù)語叫 "動態(tài)方法解析",在這個方法中膳算,系統(tǒng)會將未處理的那個 @selector(方法) 名傳遞給我們,然后讓我們返回一個 BOOL 值弛作,詢問是否能夠添加一個實例或者類方法來處理它涕蜂,當(dāng)我們直接返回一個 NO ,或者直接調(diào)用父類方法讓父類處理它時,自然就會崩掉映琳,舉個例子:
Person *p = [[Person alloc]init];
objc_msgSend(p,@selector(eat));
在上面的代碼中机隙,Person 這個類里是沒有 eat 這個實例方法的,再看下面:
@implementation Person
+ (BOOL)resolveInstanceMethod:(SEL)sel{
return [super resolveInstanceMethod:sel];
}
@end
在上面的代碼中萨西,我直接調(diào)用了父類的處理方法有鹿,當(dāng)然,這其實和不寫是沒有區(qū)別的谎脯,我們現(xiàn)在看看運行后的報錯信息
reason: '-[Person eat]: unrecognized selector sent to instance 0x7ff64350fdf0'
不管英語是好是差葱跋,作為一個程序員,看到這種報錯信息,哪怕你不懂英語也能看出來錯誤原因了吧 "向?qū)嵗l(fā)送了一個未識別的消息" ,那個 0x7ff64350fdf0 也就是當(dāng)前 p 對象的地址娱俺,其實報錯信息里還有一條重要的信息:
0 CoreFoundation 0x0000000105896d85 __exceptionPreprocess + 165
1 libobjc.A.dylib 0x000000010530adeb objc_exception_throw + 48
2 CoreFoundation 0x000000010589fd3d -[NSObject(NSObject) doesNotRecognizeSelector:] + 205
其中的 [NSObject(NSObject) doesNotRecognizeSelector:] + 205 這個方法應(yīng)該說是消息傳遞過程中的最后一步稍味,當(dāng)沒有任何措施處理這個未識別的消息,便會調(diào)用 NSObject的doesNotRecognizeSelector:方法默認實現(xiàn)來拋出異常荠卷。
那么模庐,如何在第一階段來攔截這個消息轉(zhuǎn)發(fā)的過程,進行 "動態(tài)方法解析呢" ? OK僵朗,現(xiàn)在我們來試一下:
@implementation Person
void change(){
NSLog(@"我是被添加的方法");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
class_addMethod(self, sel, (IMP)change, "v@:");
return YES;
}
@end
在上面的代碼中,我給它創(chuàng)建了一個 change 函數(shù)赖欣,在 resolveInstanceMethod 方法中給它添加進了 class_addMethod 這個函數(shù),這個函數(shù)中验庙,第一個參數(shù)是需要添加方法的類顶吮,第二個參數(shù)是需要添加的方法名,第三個參數(shù)是指向函數(shù)的IMP指針悴了,第四個參數(shù)是待添加方法的類型編碼(類型編碼:編碼開頭的字符表示方法的返回值類型,后面的字符表示各個參數(shù)),并且將 resolveInstanceMethod: 方法返回了 YES ,表示能夠動態(tài)的添加方法來處理這個未識別的消息违寿,這樣一來湃交,控制臺便會打印 change 函數(shù)中打印的那段字符串。
好了藤巢,剛剛前面部分其實說的都是第一階段搞莺,那部分報錯信息應(yīng)該說是第二階段的末尾部分,大家先別關(guān)注掂咒,接著往下看才沧。
現(xiàn)在說第二階段,如果在第一階段時绍刮,接收者沒有進行 "動態(tài)方法解析" ,那么就會進入第二階段温圆,這時接收者已經(jīng)無法再利用動態(tài)添加方法來處理未識別的消息了,但是孩革,這時候接收者也可以用其他手段來處理這條消息岁歉,這時的第二階段又得分為兩步,第一步膝蜈,運行時系統(tǒng)會詢問接收者是否有其他對象來幫助其處理這條消息锅移, 具體會調(diào)用當(dāng)前類的如下方法進行詢問:
- (id)forwardingTargetForSelector:(SEL)aSelector{
}
從上面的代碼中可以看到,該方法會返回一個 id 類型的對象饱搏,意思就是帆啃,如果有其他對象可以幫助它處理這條消息,就將那個對象返回窍帝,如果沒有,就返回 nil诽偷,但是坤学,請注意疯坤,剛剛也說了,在這第二階段深浮,已經(jīng)沒有辦法在這里給它動態(tài)添加方法了压怠,所以千萬不要把其寫在這個方法中。
經(jīng)常剛剛的第二階段的第一步操作后飞苇,如果已經(jīng)返回了可以處理該消息的對象菌瘫,那么該消息就被處理,過程便結(jié)束布卡。但是如果返回的是 nil雨让, 那么就會進入到第二步,也就是啟動 "完整的消息轉(zhuǎn)發(fā)機制" 了,這時忿等,首先會創(chuàng)建一個 NSInvocation 類型的對象栖忠,可以點到頭文件去看,這個對象里包含了 selector 贸街、target(調(diào)用目標) 以及參數(shù)庵寞,當(dāng)創(chuàng)建了這個對象,運行時會把消息傳遞給這個對象中薛匪,并且調(diào)用下面的方法來轉(zhuǎn)發(fā)消息:
- (void)forwardInvocation:(NSInvocation *)anInvocation{
}
在上面的代碼中捐川,你只需要在實現(xiàn)里改變 anInvocation 對象的 target (調(diào)用目標) 屬性,讓消息的在新的目標上得到實現(xiàn)即可逸尖,其實這個跟上面的第一步是差不多的古沥,也很少有人用到這些,如果依然沒有新的目標來處理這個消息冷溶,這個類就會按照繼承體系渐白,一直向上尋找有沒有父類及以上處理了forwardInvocation:這個方法,直至 NSObject 類逞频,如果一直到NSObject 類之前都沒有哪個類來處理纯衍,那么調(diào)用完 NSObject 類這個方法后,繼而調(diào)用 NSObject 類的 doesNotRecognizeSelector 方法苗胀,拋出異常襟诸,表明這個消息最終沒有被處理。
消息轉(zhuǎn)發(fā)部分我要說的基本就這么多基协,下面這張圖很好的表達了消息轉(zhuǎn)發(fā)的整個過程:
其實歌亲,我們在開發(fā)中基本上不需要管第二階段的那些流程,最好的方式都是在第一階段中的動態(tài)添加方法中給它處理好澜驮,當(dāng)然陷揪,筆者說的這些都是自己的看法,有不對的地方也希望各位能夠指出,??