消息轉(zhuǎn)發(fā)概述
Objective-C是一門動態(tài)語言,怎么理解動態(tài)這一詞呢?簡單的說就是編譯器在編譯期可以只知道一個方法的名字,而不需要知道這個方法的實現(xiàn),只有在運行期間調(diào)用該方法的時候,才根據(jù)方法名去找到對應(yīng)方法的實現(xiàn),這個過程相當(dāng)于動態(tài)綁定一個方法的實現(xiàn),這就是“動態(tài)”。
與“動態(tài)”相對的是“靜態(tài)”,C語言就是一門靜態(tài)語言,在編譯期不僅知道運行時所要調(diào)用的函數(shù)的名字,而且直接生成了調(diào)用函數(shù)的指令,將函數(shù)地址硬編碼在這些指令中。這就是為什么OC的方法沒有實現(xiàn)只有聲明編譯不報錯,而C的方法卻報錯的原因。
正因為是動態(tài)綁定,所以在編譯期沒有報錯的程序,在運行時由于根據(jù)方法名找不到對應(yīng)的方法實現(xiàn),會導(dǎo)致程序的Crash,在Crash之前程序會依次調(diào)用幾個其他的方法,這就引出了消息轉(zhuǎn)發(fā)。
消息轉(zhuǎn)發(fā)過程
消息轉(zhuǎn)發(fā)是Objective-C語言的特點,當(dāng)一個對象在運行時接收到無法解讀的消息時,就會觸發(fā)“消息轉(zhuǎn)發(fā)”。
在編譯期向類發(fā)送無法解讀的消息是不會報錯的,因為在運行期可以繼續(xù)向類添加方法,所以在編譯期,編譯器無法知道到底有沒有某個方法的實現(xiàn)。
可能聽起來有點亂,又是向?qū)ο蟀l(fā)送無法解讀的消息,又是向類發(fā)送無法解讀的消息。前者可理解為只在運行期有的行為(因為有對象,那肯定是調(diào)用了生成對象方法的實現(xiàn)才可能存在,而只有運行期才去調(diào)用方法的實現(xiàn)),后者看下面這句代碼就知道了[self performSelector:@selector(humenName)];方法“humenName”我并沒有聲明也沒有實現(xiàn),但是這句代碼不會報錯(會報警告),編譯也可以通過,這就是所謂的編譯期向類發(fā)送無法解讀的消息。
消息轉(zhuǎn)發(fā)的過程是有一定的規(guī)則和步驟的。下面我們看看詳細(xì)的流程。
1.先看一個runtime庫的方法 class_addMethod
class_addMethod的用處是在程序運行時,給一個類添加方法實現(xiàn)的API,其完整API如下:
/**
* 根據(jù)指定的名字和方法實現(xiàn)給一個類添加方法.
*
* @param cls 被添加方法的類.
* @param name 指定要添加的方法名稱的選擇器。
* @param imp 新的方法實現(xiàn),這個方法必須至少帶有兩個參數(shù):self和_cmd
* @param types 上面那個新方法的參數(shù)的類型編碼.
*
* @return 當(dāng)方法添加成功返回YES , 否則返回NO
* (例如,在類中已經(jīng)有一個該名字的方法實現(xiàn),會返回NO)
*
* @note class_addMethod 可能會覆蓋超類實現(xiàn), 如果超類也實現(xiàn)了該方法的話,
* 但是不會替換在本類中已經(jīng)存在的方法實現(xiàn),
* 如果改變本類存在的方法實現(xiàn),請使用method_setImplementation.
*/
OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types)
這個API在消息轉(zhuǎn)發(fā)中用到。消息轉(zhuǎn)發(fā)分為三個階段,即“動態(tài)方法解析”、“快速消息轉(zhuǎn)發(fā)”和“完整消息轉(zhuǎn)發(fā)機制”。
2. 動態(tài)方法解析
這里結(jié)合一個實例來說明,可能會更加容易懂些,新建一個項目,創(chuàng)建一個類HumenModel(繼承自NSObject),然后在ViewController中添加如下代碼
HumenModel *model = [HumenModel new];
[model performSelector:@selector(humenName)];
因為在HumenModel類中并沒有方法humenName的聲明和實現(xiàn),所以,對象model會接受到一個無法解析的消息,此時就會進入消息轉(zhuǎn)發(fā)的第一階段,征詢接收者所屬的類,看是否能動態(tài)添加方法,以處理這個未知的“選擇器”,此時就調(diào)用該類的類方法
+(BOOL)resolveInstanceMethod:(SEL)sel
當(dāng)然,如果是類接收到一個無法解析的消息,消息轉(zhuǎn)發(fā)第一階段調(diào)用的是類的另一個方法:
+(BOOL)resolveClassMethod:(SEL)sel
方法的參數(shù) sel 就是那個未知“選擇器”,返回值是BOOL類型,表示是否新增一個方法來處理未知“選擇器”。 現(xiàn)在我們就可以通過class_addMethod方法給類添加一個方法來處理未知“選擇器”,代碼如下:
+(BOOL)resolveInstanceMethod:(SEL)sel{
// 獲取選擇器的方法名字
NSString *selString = NSStringFromSelector(sel);
if ([selString isEqualToString:@"humenName"]) {
// 給接收者self 添加一個方法sayHello,選擇器sel指向方法的實現(xiàn),方法的類型編碼是v@:
class_addMethod(self, sel, (IMP)sayHello, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
// 一個c函數(shù)
void sayHello(id self, SEL _cmd){
NSLog(@"hello");
}
解釋下class_addMethod方法的第四個參數(shù)“v@:”
這是sayHello方法的OC類型編碼,‘v’表示返回值為void類型,‘@’表示第一個參數(shù)是對象,‘:’表示第二個參數(shù)是SEL類型的值,其中'@'和':'是固定的,因為每個方法都會有這兩個參數(shù)。對于其他情況可參照類型編碼這篇博客https://blog.csdn.net/ssirreplaceable/article/details/53376915
注意,所添加的方法必須是純C函數(shù)實現(xiàn)的,因為OC的方法名規(guī)則和C函數(shù)名規(guī)則差別是很大的。另外,在運行時,方法resolveInstanceMethod中的代碼會被動態(tài)插在類里面.
編譯運行,打印結(jié)果如下:
2019-02-20 17:31:53.411603+0800 MessageTrans[400:8535618] hello
跟預(yù)期的結(jié)果一樣,這樣就完成了動態(tài)方法解析,無法解析的消息在這一步得到了處理。如果在這階段沒有針對未知“選擇器”的做出處理,那么就會進入消息轉(zhuǎn)發(fā)的第二階段。
3.快速消息轉(zhuǎn)發(fā)機制
在這一階段,接收者將要甩鍋,看有沒有別的接收者可以處理這個無法解析的消息(記得將第一階段的代碼注釋掉)。這個一過程會在接收者所在類的下面這個方法中完成
-(id)forwardingTargetForSelector:(SEL)aSelector
參數(shù)aSelector是未知選擇器,返回值是id類型的值,所以這一階段只是針對對象來處理,不考慮類方法。
新建一個AnimalModel類,在其實現(xiàn)文件中實現(xiàn)方法humenName,如下:
-(void)humenName{
NSLog(@"%s",__FILE__);
}
然后在HumenModel.m中實現(xiàn)forwardingTargetForSelector:方法,如下:
-(id)forwardingTargetForSelector:(SEL)aSelector{
NSString *aSelectorString = NSStringFromSelector(aSelector);
if ([aSelectorString isEqualToString:@"humenName"]) {
return [AnimalModel new];
}
return [super forwardingTargetForSelector:aSelector];
}
這樣消息交由AnimalModel類處理,運行一下,打印結(jié)果如下:
2019-02-20 17:36:49.000246+0800 MessageTrans[2513:8550047] /Users/xiangzuhua/Desktop/MessageTrans/MessageTrans/AnimalModel.m
結(jié)果正確,我們在第二階段成功的將消息轉(zhuǎn)發(fā)給其他接收者來處理。
對于AnimalModel類,不需要在其頭文件中聲明humenName方法,沒有影響。
如果沒有其他接收者,那就會進入“完整消息轉(zhuǎn)發(fā)機制”階段。
4.完整消息轉(zhuǎn)發(fā)機制
在這個階段要處理未知消息,代價就會大些,其實也是類似于快速消息轉(zhuǎn)發(fā)階段,目的都是指定一個接受消息的對象,只不過這里必須覆蓋兩個方法,即methodSignatureForSelector:和forwardInvocation:。
methodSignatureForSelector:的作用在于為另一個類實現(xiàn)的消息創(chuàng)建一個有效的方法簽名,必須實現(xiàn),并且返回不為空的methodSignature,否則會crash。
forwardInvocation:的作用是綁定消息接收者。
看代碼實現(xiàn):
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if ([super methodSignatureForSelector:aSelector] == nil) {
// 手動創(chuàng)建
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
return signature;
}
return [super methodSignatureForSelector:aSelector];
// 自動創(chuàng)建方法簽名
// AnimalModel *animalModel = [AnimalModel new];
// return [animalModel methodSignatureForSelector:aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
AnimalModel *model = [AnimalModel new];
if ([model respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:model];
}else{
[self doesNotRecognizeSelector:anInvocation.selector];
}
}
解釋下上面的代碼:方法簽名有兩種方式,一個是手動創(chuàng)建簽名,一個是自動創(chuàng)建簽名,看代碼可以明白,著重要講的是下面這句
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
參數(shù)值"v@:"為類型編碼,前面有提到,這里就不說規(guī)則了。原則上來說,該編碼的類型應(yīng)該與未知方法的參數(shù)對應(yīng),所以用根據(jù)未知方法自動創(chuàng)建方法簽名更好。但是如果非要用手動創(chuàng)建方法簽名的話,在寫方法signatureWithObjCTypes:的參數(shù)值時要注意兩點
- 方法的返回類型必須有,不能省略,比如這里是返回空類型,所以對應(yīng)第一個編碼為v
- 方法的默認(rèn)參數(shù)self和_cmd對應(yīng)的類型編碼不能寫錯,固定為"@:"
滿足上面兩點簽名就會有效,否則會導(dǎo)致crash,至于方法簽名中參數(shù)個數(shù)與未知方法不對應(yīng)是沒有問題的,比如說類型編碼為"v@:@@@",而未知方法為:humenName(沒有參數(shù)),是不會導(dǎo)致crash,只不過這里的參數(shù)寫多少個,會影響方法forwardInvocation:的參數(shù)值anInvocation的變化。
在第二個方法forwaidInvocation:必須判斷接收者是否能響應(yīng)未知消息,否則直接執(zhí)行[anInvocation invokeWithTarget:model]在接收者無法響應(yīng)位置消息時會導(dǎo)致崩潰。如果指定的接收者不能響應(yīng)未知選擇器,那么沒辦法了只能拋出異常,執(zhí)行doesNotRecognizeSelector:方法,程序崩潰。
這里在講下剛才提到的,方法簽名的參數(shù)個數(shù)的問題,看下面的代碼:
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if ([super methodSignatureForSelector:aSelector] == nil) {
// 手動創(chuàng)建
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"@@:@@"];
return signature;
}
return [super methodSignatureForSelector:aSelector];
// 自動創(chuàng)建方法簽名
// AnimalModel *animalModel = [AnimalModel new];
// return [animalModel methodSignatureForSelector:aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
AnimalModel *model = [AnimalModel new];
SEL sel = anInvocation.selector;
NSMethodSignature *sign = anInvocation.methodSignature;
NSLog(@"%lu--%@",(unsigned long)sign.numberOfArguments,NSStringFromSelector(sel));
if ([model respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:model];
}else{
[self doesNotRecognizeSelector:anInvocation.selector];
}
}
AnimalModel類中humenName方法實現(xiàn)如下:
-(void)humenName{
NSLog(@"%s",__FILE__);
}
運行打印結(jié)果如下
2019-02-21 15:31:41.851617+0800 MessageTrans[14393:9247960] 4--humenName
2019-02-21 15:31:48.716371+0800 MessageTrans[14393:9247960] /Users/xiangzuhua/Desktop/MessageTrans/MessageTrans/AnimalModel.m
根據(jù)打印結(jié)果可以看到,方法簽名中參數(shù)個數(shù),與anInvocation中的參數(shù)個數(shù)值是對應(yīng)的,并不與未知選擇器humenName的參數(shù)個數(shù)對應(yīng)(參數(shù)個數(shù)為0),而且這種不對應(yīng)并不會影響AnimalModel類中方法humenName的正常執(zhí)行。
拓展:如果對象調(diào)用一個未指定參數(shù)值的未知消息,但是在另一個類中有該方法的實現(xiàn),會怎樣呢
剛才我們調(diào)用的未知方法是沒有參數(shù)的,我們實現(xiàn)下有參數(shù)不指定值得代碼
viewController.m -> viewDidLoadHumenModel *model = [HumenModel new]; [model performSelector:@selector(humenName:)];AnimalModel.m
-(void)humenName:(NSInteger)number{ NSLog(@"%s - %ld",__FILE__,(long)number); }HumenModel.m中的代碼和上面的相同,編譯運行,發(fā)現(xiàn)崩潰了,原因容易想到是這個消息沒有參數(shù)值,當(dāng)在AnimalModel中調(diào)用humenName:時,就會報野指針。這個問題怎么解決呢,見下面的代碼
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ // 自動創(chuàng)建方法簽名 AnimalModel *animalModel = [AnimalModel new]; return [animalModel methodSignatureForSelector:aSelector]; } -(void)forwardInvocation:(NSInvocation *)anInvocation{ AnimalModel *model = [AnimalModel new]; // 給anInvocation設(shè)置參數(shù)值 NSInteger number = 10; [anInvocation setArgument:&number atIndex:2];// 為什么是2,因為0是self參數(shù),1是_cmd參數(shù),返回值類型不屬于參數(shù)。 if ([model respondsToSelector:anInvocation.selector]) { [anInvocation invokeWithTarget:model]; }else{ [self doesNotRecognizeSelector:anInvocation.selector]; } }這樣再運行看結(jié)果如下:
2019-02-21 16:13:56.024182+0800 MessageTrans[15139:9320442] /Users/xiangzuhua/Desktop/MessageTrans/MessageTrans/AnimalModel.m - 10運行正常
到這一步消息轉(zhuǎn)發(fā)的整個流程就講完了。