问题: 运行如下代码程序会直接奔溃闪退,然后出现经常遇到的错误提示
unrecognized selector sent to instance 0x6000016ab390
//示例代码
NSArray * array = @[@1,@2,@3];
NSString * s = (NSString*)array;
NSString * _newStr = [s uppercaseString];
NSLog(@"_newStr--- %@" , _newStr);
这个错误提示几乎每个开发者都会遇到很多次,是由于给对象发送了一个无法识别的消息造成系统不能正常处理。
为了明白这个问题怎么回事,为了避免这种错误的出现,为了当被人问起时能够说个123来,我们只能从运行时的消息机制开始说起。
上面代码在编译阶段可以正常通过此时对象s还是NSStringl类型
, 运行时阶段s变成数组类型
此时向数组发送uppercaseString
消息必然找不到。
正常情况下,对象收到一个消息调用时在其所属的类中方法列表去查找,若有(将其添加到一个缓存列表中下次进来可以直接调用)则跳转到方法实现去执行。若无继续向上(父类)查找,依次查找如果都没有,最终会查到根对象NSObject
,然后调用其默认方法:
//- (void)doesNotRecognizeSelector:(SEL)aSelector;
- (void)doesNotRecognizeSelector:(SEL)sel {
_objc_fatal("-[%s %s]: unrecognized selector sent to instance %p",
object_getClassName(self), sel_getName(sel), self);
}
抛出异常,结束。
上面是问题及原因,下面开始正题
如果对象及其父类都没有实现目标方法,运行时系统便开始走消息转发流程。大致可分为以下步骤:
第一步、方法决议:给对象发送一个消息
+(bool)resolveInstanceMethod:(sel)
这里运行时系统询问对象(可以理解为我们),是否要使用BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
动态的添加个方法?
:ok,添加个处理方法吧。
~那好,我找到处理方法了,走了、干活去了....
:不想在这里添加方法,啥都不想干。
~奥、你不是我的合适对象 , 拜拜,我去找个备胎试试。于是,运行时开始了第二步 ->
如果在这里添加了方法,这里走完就算处理完成了。也没下面什么事了。
如果是个类方法,则调用+ (BOOL)resolveClassMethod:(SEL)sel;
。
第二步、寻找备援对象:给对象发送一个消息
-(id)forwardingTargetForselector:(sel)aselctor
:继续询问,能给个备用对象吗?
~好的,给你个新对象B拿去吧。
:好的,走了B,一起干活去....
:不给,上那给你搞个对象?没有返回 nil
~奥,拜拜,继续去找下家。 ->
如果在这里返回了一个对象,则有新的对象处理未知方法,也没有我什么事了。
注意在这里:
- 返回的对象肯定不能是自己了。
- 不能获取NSInvocation消息相关内容。
- 不能操作处理参数
- 不能处理返回值
- 只是简单的返回一个对象来处理未知方法。
第三步 、给对象发送一个消息
-(void)forwardInvocation:(NSInvocation *)anInvocation
开始转发处理
走到这一步,说明前面的操作都失败了。运行时开始处理了:把消息相关的都封装成一个NSInvocation
对象传递给当前这个方法。此时又可细分两个操作:
- a.要生成一个
NSInvocation
对象 , 我们还必须重写
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
此方法提供一个方法签名对象描述被转发的消息,运行时根据这个对象生成转发消息的NSInvocation
对象并传给forwardInvocation:
。如果返回nil
, 直接调用doesNotRecognizeSelector抛出异常,结束。
- b.然后才走到
-(void)forwardInvocation:(NSInvocation *)anInvocation
到此大致流程如下:
iOS消息转发流程.png在-(void)forwardInvocation:(NSInvocation *)anInvocation
方法中的操作
这个方法里主要做两个操作:
- 1、找到能够响应
anInvocation
对象的消息。 - 2、给对象发送消息。
执行完成后,anInvocation
对象 保存着执行结果、运行时系统获取结果,然后返回原始调用对象 , 完成处理。
此时走到这里如果你不进行如何处理也没关系了,程序也不会闪退了,结果返回null,结束。
在这里可以调用自己对象的其他方法,也可以调用其它函数,以及多个不同对象的多个方法。
如果继续处理也可以分为两种情况:
1、直接修改 anInvocation
的target属性改变调用对象。
//修改target方式1
NSString * newObject = @"a1233";
[anInvocation setTarget:newObject];
[anInvocation invoke];
//或者直接使用方法:
//修改target方式2
[anInvocation invokeWithTarget:newObject];
两种方式结果一样。但方式2更好一点,通过2我们可以把消息传给多个对象如下:
[anInvocation invokeWithTarget:newObject2];
[anInvocation invokeWithTarget:newObject3];
,
,
,
当多个对象时,返回结果以最后一个为准。
执行结果的返回值可通过- (void)getReturnValue:(void *)retLoc;
查看;
注意:
如果就这样仅仅修改了一个操作对象(不是要转发多个对象
),是不是和步骤2的寻找备援对象很像,其实功能一样。那又何必在这里多此一举呢,系统一番操作结果成了无用功而且走到这一步也很耗性能。直接在找备援对象的时候给它得了。
2、修改anInvocation
对象其他属性。
-
修改target、修改参数、修改返回值
-
不做其他操作直接添加一个我们默认的返回值。
//直接修改返回值调用方法
NSString * defaultResult = @"不能识别的方法返回的提示信息!";
[anInvocation setReturnValue:&defaultResult];
总结
了解以上这一处理流程能运行时的消息转发
机制有了更深入的理解,基于此我们可以额外做一些操作;
- 我们可以让对象响应本身不属于自己的方法,让外界看起来这个对象继承了其他类的特性也就是所谓的模拟多继承(众所周知iOS本身是不支持多继承的)。
- 声明类的属性为dynamic,自己添加相应的处理方法。
- 为不能识别的方法添加默认的处理。
iOS开发技术基础总结:巩固基础、重塑自我!
网友评论