iOS 消息转发机制(VN的逃生之路)
故事背景:在德玛西亚的战场上,硝烟弥漫,紫色方英雄薇恩正在河道处戏弄一只毫无攻击性的螃蟹,丝毫没有感觉到附近的杀气。突然,从草丛中冒出敌方四员大将,只听其中一名怒吼:“德玛西亚!!!”......
描述此故事前,先附上一张故事的整体流程图:
消息转发.png这里有一个类ADCHero
, 有四个方法,分别是skillQ, skillW, skillE, skillR, 但是skillR方法没有实现。
在ADCHero.h 文件
@interface ADCHero : NSObject
// Q技能
- (void)skillQ;
// W技能
- (void)skillW;
// E技能
- (void)skillE;
// R技能
- (void)skillR;
@end
在ADCHero.m文件
@implementation ADCHero
- (void)skillQ {
NSLog(@"ADC 发起了Q技能");
}
- (void)skillW {
NSLog(@"ADC 发起了W技能");
}
- (void)skillE {
NSLog(@"ADC 发起了E技能");
}
@end
创建一个薇恩对象
ADCHero *vn = [[ADCHero alloc] init];
薇恩在面临生命危险的时候,准备逃跑,于是准备开启大招R 进入隐身状态。
调用方法
[vn skillR];// 直接运行程序,报错: unrecognized selector sent to instance 0x170006e00 程序崩溃,因为找不到方法实现。
在Objective-C中对象调用方法,实际上是给对象发送消息,在底层会调用objc_msgSend方法
// 第一个参数是消息的接收者; 第二个参数是要调用的方法名; 后面的参数依次是调用的方法中的参数。
objc_msgSend(receiver, selector, arg1, arg2, …) :
结局一: 可是薇恩忘了自己没有加R的技能点,使用不出R技能,于是在敌方四人的围殴下,壮烈牺牲。(程序崩溃)
打印信息:
程序崩溃为了不让薇恩这么快就死了,Objective-C做了一些处理(消息转发)
在薇恩没法使用R技能时,先询问薇恩,能否使用其它的技能逃跑。薇恩想:“我没有R技能,那我直接闪现逃跑吧”。情形如下。
此时会调用ADCHero
类的类方法resolveInstanceMethod
, 在这个方法中动态添加其它的方法。
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"%s", __func__);
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString isEqualToString:@"skillR"]) { // 如果方法名是skillR
class_addMethod(self, sel, (IMP)skillFlash, "@:"); // 动态添加方法skillFlash, 参数一: 消息接收者;参数二: 调用的方法名;参数三:方法对应的实现地址;参数四: 类型编码。
return YES; //
}
return [super resolveInstanceMethod:sel];
}
void skillFlash() {
NSLog(@"闪现");
}
打印信息:
结果薇恩使用闪现极限逃生。。。 但是,故事的情节有变化,有的时候闪现是处于CD状态,也无法使用,不能够闪现逃走,vn在想:"要不找队友支援吧!",于是把消息发给了队友日女。正巧日女从附近焦急地赶过来。
在代码中,将resolveInstanceMethod
的方法内部更改一下。并重写forwardingTargetForSelector
方法, forwardingTargetForSelector
方法内部,创建了一个辅助英雄日女,该类中有方法skillR
,并且已经实现。
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"%s", func);
return NO;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s", __func__);
NSString *selectorString = NSStringFromSelector(aSelector);
if ([selectorString isEqualToString:@"skillR"]) {
AsistantHero *rinv = [[AsistantHero alloc] init];
return rinv;
}
// 如果队友不在身边
return [super forwardingTargetForSelector:aSelector];
}
AsistantHero
类.h .m文件如下:
// AsistantHero.h文件
@interface AsistantHero : NSObject
// Q技能
- (void)skillQ;
// W技能
- (void)skillW;
// E技能
- (void)skillE;
// R技能
- (void)skillR;
@end
// AsistantHero.m 文件
@implementation AsistantHero
- (void)skillQ {
NSLog(@"SUP 发起了Q技能");
}
- (void)skillW {
NSLog(@"SUP 发起了W技能");
}
- (void)skillE {
NSLog(@"SUP 发起了E技能");
}
- (void)skillR {
NSLog(@"SUP 发起了R技能 保护了ADC");
}
@end
运行程序,打印信息如下:
打印信息日女使用R技能减速了敌方四人,并救援薇恩 顺利逃走。。。。。。但是,故事的情节又有变化,由于在下路对线的时候ADC薇恩和辅助日女产生了分歧,导致日女心里不太爽,于是日女不大想救薇恩。结果薇恩又死了。
在代码中,将forwardingTargetForSelector
方法内部修改一下:
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s", __func__);
return [super forwardingTargetForSelector:aSelector];
}
运行程序,打印信息如下:
打印信息然而,故事情节又有转机,薇恩告诉日女:“如果你不救我,我就挂机!”,日女考虑到这把是自己的晋级赛,于是心想:“就救他一次吧,反正救人一命胜造七级浮屠。”
在代码中, 重写methodSignatureForSelector
方法和forwardInvocation
方法.
// 完整的消息转发机制
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s", __func__);
NSString *selectorString = NSStringFromSelector(aSelector);
if ([selectorString isEqualToString:@"skillR"]) {
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
return signature;
}
return nil;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"%s", __func__);
AsistantHero *rinv = [[AsistantHero alloc] init];
if ([rinv respondsToSelector:[anInvocation selector]]) {
[anInvocation invokeWithTarget:rinv];
} else {
[super forwardInvocation:anInvocation];
}
}
运行程序,结果如下:
打印结果最后日女抓住了最后一次机会,救下了薇恩,薇恩为了表达感激之情,让他的王者表哥带日女打赢了晋级赛。
总结一下,在上面的故事中,薇恩面对敌方四人埋伏,自己又没有R技能逃亡。如何使用iOS的消息转发机制一步一步的惊险逃脱。当他调用 [vn skillR]方法时,究竟做了哪些事。
第一阶段,看自己能不能在接受到这个消息时,使用闪现逃跑,如果不能,进入到第二个阶段。
第二阶段,看有没有辅助在身旁替自己接受这个消息,如果有就把消息传递给辅助,让辅助救援他。如果没有,则进入第三个阶段。
第三阶段,把消息封装起来,告诉辅助,给他最后一次机会,让他设法处理。
对应与消息转发机制,iOS消息转发分为三大阶段:
- 第一阶段,先征询消息接收者所属的类,看其是否能动态添加方法,以处理当前这个无法响应的selector, 及动态方法解析。如果运行期系统 第一阶段执行结束,接收者就无法再以动态新增方法的手段来响应消息,进入第二阶段。
- 第二阶段,看看有没有其它对象(备用接收者)能处理此消息。如果有,运行期系统会把消息发给那个对象,转发过程结束;如果没有,则启动完整的消息转发机制。
- 第三阶段,完整的消息转发机制。运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的问题。
参考资料:
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Introduction/Introduction.html
https://hit-alibaba.github.io/interview/iOS/ObjC-Basic/Runtime.html