OC 消息转发机制
首先我们看一下objc_msgSend它具体是如何发送消息:
- 首先根据receiver对象的isa指针获取它对应的class
- 优先在class的cache查找message方法,如果找不到,再到
methodLists查找 - 如果没有在class找到,再到super_class查找
-
一旦找到message这个方法,再依据receiver 中的self 指针找到当前的对象,调用当前对象的具体实现的方法(IMP),然后传递参数,调用实现方法。
图1
当对象收到无法解读的消息后,就会启动“消息转发”(message forwarding)机制,程序员可经由此过程告诉对象应该如何处理未知消息。
消息转发全流程图:
图2方案一:
+ (BOOL)resolveInstanceMethod:(SEL)sel
+ (BOOL)resolveClassMethod:(SEL)sel
首先,系统会调用resolveInstanceMethod
(当然,如果这个方法是一个类方法,就会调用resolveClassMethod
)让你自己为这个方法增加实现。
demo1:
定义一个Person,创建了一个Person类的对象p,然后调用p的run方法,注意,这个run方法是没有写实现的。
Person *p = [Person alloc] init];
[p run];
进入Person类的.m文件,我实现了resolveInstanceMethod
这个方法为我的Person类动态增加了一个run方法的实现。
void run (id self, SEL _cmd) {
NSLog(@"跑");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if(sel == @selector(run)){
class_addMethod(self, sel, (IMP)run, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
方案二:
- (id)forwardingTargetForSelector:(SEL)aSelector
第二套方法,forwardingTargetForSelector
,这个方法返回你需要转发消息的对象。
demo2:
为了便于演示消息转发,我们新建了一个汽车类Car,并且实现了Car的run方法。
现在我不去对方案一的resolveInstanceMethod做任何处理,直接调用父类方法。可以看到,系统已经来到了forwardingTargetForSelector
方法,我们现在返回一个Car类的实例对象。
- (id)forwardingTargetForSelector:(SEL)aSelector {
return [[Car alloc] init];
}
继续运行,程序就来到了Car类的run方法,这样,我们就实现了消息转发。
方案三:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector;
- (void)forwardInvocation:(NSInvocation *)invocation;
methodSignatureForSelector
用来生成方法签名,这个签名就是给forwardInvocation
中的参数NSInvocation
调用的。
开头我们要找的错误unrecognized selector sent to instance
原因,原来就是因为methodSignatureForSelector
这个方法中,由于没有找到run对应的实现方法,所以返回了一个空的方法签名,最终导致程序报错崩溃。
所以我们需要做的是自己新建方法签名,再在forwardInvocation
中用你要转发的那个对象调用这个对应的签名,这样也实现了消息转发。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
NSString *sel = NSStringFromSelector(selector);
if([sel isEqualString:@"run"]) {
return [NSMethodSignature signatureWithObjcTypes:"v@:"];
}
return [sunper methodSignatureForSelector:selector];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
SEL selector = [invocation selector];
//新建需要转发消息的对象
Car *car = [[Car alloc] init];
if([car respondsToSelector:selector]){
[invocation invokeWithTarget:car];
}
}
关于生成签名的类型"v@:"解释一下。每一个方法会默认隐藏两个参数,self、_cmd,self代表方法调用者,_cmd代表这个方法的SEL,签名类型就是用来描述这个方法的返回值、参数的,v代表返回值为void,@表示self,:表示_cmd。
注意一点,前一套方案实现后一套方法就不会执行。如果这几套方案你都没有做处理,那么程序就会报错crash。