消息轉(zhuǎn)發(fā)機(jī)制,顧名思義,就是消息被轉(zhuǎn)發(fā)了,但是這個(gè)轉(zhuǎn)發(fā)呢,有一個(gè)制度需要遵循,廢話少說(shuō),直接進(jìn)入正題。
首先我們了解一下消息傳遞的形式
object-c是一門動(dòng)態(tài)語(yǔ)言,我們?cè)趏bject-c中每次調(diào)起一個(gè)方法就是想這個(gè)對(duì)象發(fā)送一條消息,也就是進(jìn)行一次消息傳遞,比方說(shuō):
[self sendMsg:@"參數(shù)"];
此時(shí),在編譯時(shí)調(diào)起object-c函數(shù)就會(huì)轉(zhuǎn)化為調(diào)起c函數(shù),在解析這條消息之前,我們先來(lái)看一下消息傳遞機(jī)制中所調(diào)用的核心函數(shù):
void objc_msgSend(id self, SEL cmd, ......)
解釋一下各個(gè)參數(shù)代表的意思,第一個(gè)參數(shù):消息接收者;第二個(gè)參數(shù):選擇子,它最少需要帶兩個(gè)參數(shù),后面的參數(shù)可以是消息傳遞的參數(shù),數(shù)量不限。
上面的函數(shù)調(diào)起時(shí)會(huì)轉(zhuǎn)化為如下形式發(fā)送消息:
object_msgSend(id self, SEL sendMsg,@"參數(shù)")
括號(hào)中三個(gè)參數(shù)的意義:
1、 第一個(gè)參數(shù)是接收者,指的是調(diào)起參數(shù)的類或者對(duì)象;
2、 第二個(gè)參數(shù)是選擇子,指的是類或者對(duì)象調(diào)用的方法;
3、 第三個(gè)參數(shù)是方法中需要傳遞的參數(shù),可以為一個(gè),也可以為多個(gè)。
了解了傳遞的形式,我們?cè)賮?lái)看一下消息轉(zhuǎn)發(fā)的機(jī)制
消息轉(zhuǎn)發(fā)機(jī)制的形式
消息轉(zhuǎn)發(fā)機(jī)制有三種方式
1、動(dòng)態(tài)方法解析階段
(1)+ (BOOL)resolveClassMethod:(SEL)sel
(2)+ (BOOL)resolveInstanceMethod:(SEL)sel
(1)是類方法解析,(2)是對(duì)象方法解析。如果動(dòng)態(tài)方法解析找到了這個(gè)方法,就會(huì)返回yes,如果沒有找到這個(gè)方法,就會(huì)進(jìn)入第二個(gè)階段
2、備援接收階段
-(id)forwardingTargetForSelector:(SEL)aSelector
在這個(gè)方法中我們可以把a(bǔ)Selector這個(gè)方法轉(zhuǎn)發(fā)給其他對(duì)象來(lái)調(diào)用,如果沒有備援接收者,那么,對(duì)于消息的處理就會(huì)進(jìn)入第三個(gè)階段
3、完整消息轉(zhuǎn)發(fā)階段
-(void)forwardInvocation:(NSInvocation *)anInvocation
這個(gè)方法主要是轉(zhuǎn)發(fā)給多個(gè)處理對(duì)象,和第二個(gè)階段類似,只是第二個(gè)階段是轉(zhuǎn)發(fā)個(gè)一個(gè)對(duì)象,而第三個(gè)階段是轉(zhuǎn)發(fā)個(gè)多個(gè)對(duì)象。
介紹完了消息轉(zhuǎn)發(fā)的三種方式,現(xiàn)在我們看一下具體的實(shí)現(xiàn)。
一、動(dòng)態(tài)方法解析階段的實(shí)現(xiàn)
首先,我們先新建一個(gè)工程,具體名字根據(jù)個(gè)人喜好來(lái)定,我起的名字是RuntimeWithMsgSend。
我在工程中新建了三個(gè)類,都繼承自NSObject,分別為Cat類,Dog類,Rabbit類,然后我又新建了一個(gè)繼承自UIViewController類的AnimalsViewController類,用來(lái)實(shí)現(xiàn)Cat類、Dog類和Rabbit類的調(diào)用。
我先在Cat.h文件中聲明一個(gè)sayHello的對(duì)象方法,但是沒有在Cat.m文件中并沒有實(shí)現(xiàn)這個(gè)方法,我們可以看到在Cat.m文件中有一個(gè)警告,如下圖:
我們?cè)贏nimalsViewController中使用Cat類的對(duì)象去調(diào)用sayHello方法,如下:
- (void)viewDidLoad {
[super viewDidLoad];
Cat *cat = [[Cat alloc] init];
[cat sayHello];
}
運(yùn)行結(jié)果如下:
程序crash了,說(shuō)的是找不到這個(gè)選擇器,當(dāng)然,我們學(xué)習(xí)寫程序并不是為了制造bug的,但是,如果出現(xiàn)了這種bug我們應(yīng)該怎么辦呢?我們可以使用runtime來(lái)防止程序crash,這是它的作用之一。當(dāng)我們cat這個(gè)對(duì)象調(diào)用sayHello方法找不到的時(shí)候,就會(huì)進(jìn)入動(dòng)態(tài)方法解析階段,我們需要重寫+ (BOOL)resolveInstanceMethod:(SEL)sel這個(gè)方法,只要在這個(gè)方法中做一些操作,對(duì)傳過(guò)來(lái)的選擇子進(jìn)行處理,就有可能避免程序crash,在我們視線此方法之前,我們需要倒入頭文件:#import <objc/runtime.h>,方法的具體實(shí)現(xiàn)如下:
void say(id self, SEL _cmd){
NSLog(@"say-----%@",self);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if ([NSStringFromSelector(sel) isEqualToString:@"sayHello"]) {
// class_addMethod([self superclass], sel, (IMP)say, "v@:");
class_addMethod([self class], sel, (IMP)say, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
其中,if判斷語(yǔ)句中是判斷當(dāng)前方法是否是你要?jiǎng)討B(tài)添加的方法,而class_addMethod(Class _Nullable __unsafe_unretained cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)是runtime中的方法,它的作用是向該類中的實(shí)例對(duì)象添加相應(yīng)的方法實(shí)現(xiàn),四個(gè)參數(shù)分別代表
- cls 想要添加的類對(duì)象
- name 添加后的方法名
- imp 具體的實(shí)現(xiàn)方法
- types 方法參數(shù)的編碼
我們?cè)俅芜\(yùn)行程序,日志如下:
此時(shí),程序沒有crash,而是走了say這個(gè)方法,這是因?yàn)槲覀冊(cè)谶@個(gè)+ (BOOL)resolveInstanceMethod:(SEL)sel方法中給cat這個(gè)對(duì)象重新動(dòng)態(tài)添加了這個(gè)方法,所以,程序沒有崩潰,而是走了-(void)say這個(gè)方法。
二、備援接收階段實(shí)現(xiàn)
第二個(gè)階段我是在Dog類中進(jìn)行實(shí)現(xiàn)的,首先我在Dog.h文件中聲明了一個(gè)方法,它和Cat.h文件中的方法同名,同樣的,沒有在Dog.m文件中實(shí)現(xiàn)此方法,我們知道,如果對(duì)象在調(diào)用一個(gè)方法的時(shí)候沒有實(shí)現(xiàn)的話,那么程序會(huì)crash,可是我們可以通過(guò)重寫+ (BOOL)resolveInstanceMethod:(SEL)sel這個(gè)方法來(lái)防止crash,如果我們?cè)谶@個(gè)方法中返回No,那么,消息轉(zhuǎn)發(fā)就會(huì)進(jìn)入第二個(gè)階段,我們?cè)贒og.m中做如下操作:
+ (BOOL)resolveInstanceMethod:(SEL)sel{
return NO;
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
// if (aSelector == @selector(sayHello)) {
// return [[Cat alloc] init];
// }
if ([NSStringFromSelector(aSelector) isEqualToString:@"sayHello"]) {
return [[Cat alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
若方法是sayHello我們就創(chuàng)建一個(gè)接收者,然后改變此消息的接受者,只要在接受者那里有這個(gè)方法,程序就不會(huì)crash。我們?cè)贏nimalsViewController.m中實(shí)現(xiàn)調(diào)用此方法,代碼如下:
- (void)viewDidLoad {
[super viewDidLoad];
Cat *cat = [[Cat alloc] init];
[cat sayHello];
Dog *dog = [[Dog alloc] init];
[dog sayHello];
}
運(yùn)行結(jié)果如下:
我們可以看到,不管哪個(gè)對(duì)象調(diào)用sayHello方法,走的都是say方法。
三、完整消息轉(zhuǎn)發(fā)階段實(shí)現(xiàn)
當(dāng)前兩個(gè)階段都沒有成功處理這個(gè)消息時(shí),程序就會(huì)進(jìn)入第三個(gè)階段,此時(shí)我們使用Rabbit類來(lái)做具體的實(shí)現(xiàn)。首先,我們?cè)赗abbit.h中還是聲明-(void)sayHellog實(shí)例方法,依然不在在Rabbit.m中實(shí)現(xiàn),而我們可以在Rabbit.m文件中實(shí)現(xiàn)-(void)forwardInvocation:(NSInvocation *)anInvocation方法,具體代碼如下:
- (void)rabbitSayHello{
NSLog(@"rabbitSayHello------%@",self);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
return NO;
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
return nil;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if ([NSStringFromSelector(aSelector) isEqualToString:@"sayHello"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
// 改變對(duì)象
[anInvocation invokeWithTarget:[[Cat alloc] init]];
// 改變選擇子
// anInvocation.selector = @selector(rabbitSayHello);
// [anInvocation invokeWithTarget:self];
}
在這里要特殊說(shuō)明一下,當(dāng)進(jìn)入第三個(gè)階段之后我們需要先實(shí)現(xiàn)一個(gè)方法:- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector,我們通過(guò)此方法對(duì)傳遞過(guò)來(lái)的消息進(jìn)行處理,此方法在- (void)forwardInvocation:(NSInvocation *)anInvocation方法之前調(diào)用,這是最后一個(gè)尋找IML的機(jī)會(huì),這個(gè)函數(shù)會(huì)讓重載方有機(jī)會(huì)拋出一個(gè)函數(shù)簽名,再由- (void)forwardInvocation:(NSInvocation *)anInvocation方法去執(zhí)行。我們?cè)贏nimalsViewController.m中調(diào)用:
- (void)viewDidLoad {
[super viewDidLoad];
Cat *cat = [[Cat alloc] init];
[cat sayHello];
Dog *dog = [[Dog alloc] init];
[dog sayHello];
Rabbit *rabbit = [[Rabbit alloc] init];
[rabbit sayHello];
}
運(yùn)行程序,打印日志如下:
從打印日志中我們可以看到,三個(gè)類最后都是調(diào)用的say方法。若三個(gè)階段都沒有成功處理sayHello方法,那么程序就會(huì)拋出crash。
消息轉(zhuǎn)發(fā)出現(xiàn)的場(chǎng)景
消息轉(zhuǎn)發(fā)出現(xiàn)在程序運(yùn)行時(shí)調(diào)用方法的時(shí)候,我們知道,當(dāng)調(diào)用的方法不存在的時(shí)候就會(huì)出現(xiàn)crash,而消息轉(zhuǎn)發(fā)機(jī)制的三個(gè)階段就是為了防止程序crash,只要我們?cè)谏鲜鋈齻€(gè)階段做了合理的處理,程序在運(yùn)行的時(shí)候就會(huì)動(dòng)態(tài)的處理這個(gè)消息,防止crash的出現(xiàn),若上述三個(gè)階段都沒有對(duì)消息進(jìn)行處理,就會(huì)進(jìn)入一個(gè)NSObject的一個(gè)方法
-(void)doesNotRecognizeSelector:(SEL)aSelector
在這個(gè)方法中捕獲異常,對(duì)異常進(jìn)行處理,獲取自己需要的信息。
若覺得此文章對(duì)您有用,記得給個(gè)喜歡呦??,[Runtime的demo]https://github.com/houcj/RuntimeWithMsgSend
附圖一張,查資料的時(shí)候考下來(lái)的