iOS中的经典错误unrecognized selector sent to instance在开发中经常遇到,报错的含义是没有找到要被调用的方法,程序发生崩溃,但是很少有人知道其中发生了什么事.
在这里会涉及到iOS的消息转发机制,那么什么是消息转发
比如 :
[person play];
这实际上这是在给person这个对象发送play这个消息.
如果这个时候在play只有方法的声明,却没有方法的实现就会发生*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person play]: unrecognized selector sent to instance
那我们来看下这其中消息是怎么处理的
231837047638961.png一.消息转发流程
-
动态方法解析(Dynamic Method Resolution或Lazy method resolution)
向当前类(Class)发送resolveInstanceMethod:(对于类方法则为resolveClassMethod:)消息,如果返回YES,则系统认为请求的方法已经加入到了,则会重新发送消息。 -
快速转发路径(Fast forwarding path)
若果当前target实现了forwardingTargetForSelector:方法,则调用此方法。如果此方法返回除nil和self的其他对象,则向返回对象重新发送消息。 -
慢速转发路径(Normal forwarding path)
首先runtime发送methodSignatureForSelector:消息查看Selector对应的方法签名,即参数与返回值的类型信息。如果有方法签名返回,runtime则根据方法签名创建描述该消息的NSInvocation,向当前对象发送forwardInvocation:消息,以创建的NSInvocation对象作为参数;若methodSignatureForSelector:无方法签名返回,则向当前对象发送doesNotRecognizeSelector:消息,程序抛出异常退出。
二.动态解析(Lazy Resolution)
runtime发送消息的流程即查找该消息对应的方法或IMP,然后跳转至对应的IMP。有时候我们不想事先在类中设置好方法,而想在运行时动态的在类中插入IMP。这种方法是真正的快速”转发”,因为一旦对应的方法被添加到类中,后续的方法调用就是正常的消息发送流程。此方法的缺点是不够灵活,你必须有此方法的实现(IMP),这意味这你必须事先预测此方法的参数和返回值类型。
@dynamic属性是使用动态解析的一个例子,@dynamic告诉编译器该属性对应的getter或setter方法会在运行时提供,所以编译器不会出现warning; 然后实现resolveInstanceMethod:方法在运行时将属性相关的方法加入到Class中。
当respondsToSelector:或instancesRespondToSelector:方法被调用时,若该方法在类中未实现,动态方法解析器也会被调用,这时可向类中增加IMP,并返回YES,则对应的respondsToSelector:的方法也返回YES。
比如,我在.m中不实现play的方法
void play (id self, SEL _cmd) {
NSLog(@"转发到这里%s",__func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(play)){
class_addMethod([self class], sel,(IMP)play, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
这个时候消息就被转发到play方法中
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
方法说明
一个函数是由一个selector(SEL),和一个implement(IML)组成的。Selector相当于门牌号,而Implement才是真正的住户(函数实现)。
三.快速转发(Fast Forwarding)
runtime然后会检查你是否想将此消息不做改动的转发给另外一个对象,这是比较常见的消息转发情形,可以用较小的消耗完成。
快速转发技术可以用来实现伪多继承,你只需编写如下代码
- (id)forwardingTargetForSelector:(SEL)sel { return _otherObject; }
这样做会将任何位置的消息都转发给_otherObject对象,尽管当前对象与_otherObject对象是包含关系,但从外界看来当前对象和_otherObject像是同一个对象。
伪多继承与真正的多继承的区别在于,真正的多继承是将多个类的功能组合到一个对象中,而消息转发实现的伪多继承,对应的功能仍然分布在多个对象中,但是将多个对象的区别对消息发送者透明。
其实这里简单理解就是如果说还没没有找方法那么会把消息发给其他的对象去寻找play方法
新增一个GHDog类,在.h中声名play方法,在.m中实现方法
- (id)forwardingTargetForSelector:(SEL)aSelector {
GHDog *dog = [[GHDog alloc] init];
if ([dog respondsToSelector: aSelector]) {
return dog;
}
return [super forwardingTargetForSelector: aSelector];
}
那么这个时候就会实现GHDog的play方法,如果说dog中也没有play方法那么会进去慢速转发
四.慢速转发(Normal Forwarding)
以上两者方式是对消息转发的优化,如果你不使用上述两种方式,则会进入完整的消息转发流程。这会创建一个NSInvocation对象来完全包含发送的消息,其中包括target,selector,所有的参数,返回值。
在runtime构建NSInvocation之前首先需要一个NSMethodSignature,所以它通过-methodSignatureForSelector:方法请求。一旦NSInvocation创建完成,runtime就会调用forwardInvocation:方法,在此方法内你可以使用参数中的invocation做任何事情.
/// methodSignatureForSelector用来生成方法签名,这个签名就是给forwardInvocation中的参数NSInvocation调用的。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
开头我们要找的错误unrecognized selector sent to instance原因,原来就是因为methodSignatureForSelector这个方法中,由于没有找到run对应的实现方法,所以返回了一个空的方法签名,最终导致程序报错崩溃。
所以我们需要做的是自己新建方法签名,再在forwardInvocation中用你要转发的那个对象调用这个对应的签名,这样也实现了消息转发。
真正执行从methodSignatureForSelector:返回的NSMethodSignature。在这个函数里可以将NSInvocation多次转发到多个对象中,这也是这种方式灵活的地方。(forwardingTargetForSelector只能以Selector的形式转向一个对象)
- (void)forwardInvocation:(NSInvocation *)anInvocation
关于生成签名的类型"v@:"解释一下。每一个方法会默认隐藏两个参数,self、_cmd,self代表方法调用者,_cmd代表这个方法的SEL,签名类型就是用来描述这个方法的返回值、参数的,v代表返回值为void,@表示self,:表示_cmd。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector OBJC_SWIFT_UNAVAILABLE(""); {
if (aSelector == @selector(play)){
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation OBJC_SWIFT_UNAVAILABLE(""){
SEL selector = [anInvocation selector];
GHDog *dog = [[GHDog alloc] init];
if ([dog respondsToSelector:selector]){
[anInvocation invokeWithTarget:dog];
}
[super forwardInvocation:anInvocation];
}
崩溃
- (void)doesNotRecognizeSelector:(SEL)aSelector
作为找不到函数实现的最后一步,NSObject实现这个函数只有一个功能,就是抛出异常。
虽然理论上可以重载这个函数实现保证不抛出异常(不调用super实现),但是苹果文档着重提出“一定不能让这个函数就这么结束掉,必须抛出异常”。
- (void)doesNotRecognizeSelector:(SEL)aSelector {
[super doesNotRecognizeSelector:aSelector];
}
所以以上代码修改为
- (void)doesNotRecognizeSelector:(SEL)aSelector {
@try {
[super doesNotRecognizeSelector:aSelector];
}
@catch (NSException *exception) {
// 捕获到的异常exception
NSString *title = [NSString stringWithFormat:@"%@%@",exception.reason,exception.name];
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:title message:nil delegate:nil cancelButtonTitle:@"知道了" otherButtonTitles: nil];
[alert show];
}
@finally {
}
}
image.png
网友评论