在iOS开发中经常会遇到unrecognized selector sent to instance 0x100111df0'
的问题,这是为什么呢,从字面上理解来说是无法识别的selector子发送给对象,其实调用一个不存在的方法就会遇到这个问题。
严格来说iOS中不存在方法调用的说法,应该说是消息的传递。
消息传递和函数调用的区别就是,你可以在任意的时候对一个对象发送任何消息,而不需要在编译的时候声明。但是函数调用就不行。
- (void)foo {
}
[self foo];
以上的是一个简单的例子,相当于向self
对象传递foo
方法,objective-C会在runtime时期将这个转换为
objc_msgSend(self, foo)
objc_msgSend(id theReceiver, SEL selectot,……)
这里的objc_msgSend
是一个可变参数的函数,接受大于等于两个参数。第一个参数是id类型的,可以是任何对象或者类。selector
是一个SEL
类型的参数。那么SEL
是什么呢?SEL是对方法的一种封装。其实就是个方法名或者说是签名,方法真正的实现在IMP
中。
方法的链表大概是这个样子。
typeof struct objc_method {
SEL method_name
IMP method_imp
……………………
}
SEL相当于门牌号,IMP相当于真正的住处,门牌号可以随便搞,但是瞎指就会出问题。
我们下来看一下在OC中传递一个消息会发生什么事情。
调用一个``objc_msgSend(id theReceiver, SEL selectot,……)`方法系统执行的步骤为:
- 判断receiver是否为nil,如果是nil的话则不往下执行,返回nil,这就是为什么在oc中一个nil发送消息不会引起奔溃。
- 1、从方法的缓存中查找 被调用过的方法会存在缓存里面,每个类都会有一个表来存被调用过的方法,以便下次更快的调用。
- 2、从本类的方法表(dispatch table)中查找方法寻找selector,找到则写入缓存,返回方法。否则再从父类中查找方法,如此往复,直到达到基类。如果找不到则执行方法的动态解析。
-
3、方法的动态解析: 调用
+ (BOOL)resolveInstanceMethod:(SEL)sel
方法来查看是否能够返回一个selector,如果存在则返回selector。不存在进入下一步。 -
4、备用接受者
- (id)forwardingTargetForSelector:(SEL)aSelector
这个方法来询问是否有接受者可以接受这个方法呀。如果有人接受,则交给它处理,就好像一切都没发生过一样。 -
5、方法的转发: 如果到这一步还不能够找到相应的Selector的话,就要进行完整的方法转发过程。调用方法
(void)forwardInvocation:(NSInvocation *)anInvocation
- 最后还是没有找到的话就只有呵呵了,这时候
unrecognized selector sent to instance 0x100111df0'
的错误就来了。
以上是处理消息的流程图。这里可以看到查找一个方法需要经过很多的步骤,所以我们很多次机会来弥补这种错误,但是越往后面处理消息所消耗的代价越大。我们从第一步开始看,最好能够在一开始就找到相应的selector,那么他就会把方法缓存起来,等再次调用相同的方法的时候就会直接从缓存中取出来,那效率很高,和直接用c调用的速度慢不了多少。在没有缓存的情况下会从类的方法表里面进行查找。一个对象会有一个isa指针来指向自己所属的类。而类则会有一个方法表(dispatch table),用于将selector和真正实现的内存地址对应起来。另外还有一个指针会指向父类,这样就可以逐级向上查找直到基类。如下图
查找顺序方法的动态解析
- (instancetype)init {
if (self = [super init]) {
[self performSelector:@selector(creash)];
}
return self;
}
这里我调用了creash,但是方法并没有被实现,所以会出错。
我们来实现下面的方法,不要忘记导入头文件#import <objc/runtime.h>
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *selectorString = NSStringFromSelector(sel);
if ([selectorString isEqualToString:@"creash"]) {
class_addMethod(self,
sel,
(IMP)askMeWhenCreash,
"");
return YES;
}
return NO;
}
void askMeWhenCreash() {
NSLog(@"creash不要慌,来执行这个");
}
在creash方法没找到之后,程序首先进入resolveInstanceMethod
方法,我们先来判断方法名是否为creash,如果是的话我们在这里用class_addMethod(Class cls, SEL name, IMP imp, const char *types)
方法动态的给他添加方法的实现。第三个参数imp就是,我们将它设为自己定义的一个方法void askMeWhenCreash()
,最后return YES
表示我们已经处理,不会再报错。
备用接受者
走到这一步我们其实能做的已经很少了,- (id)forwardingTargetForSelector:(SEL)aSelector
方法只是给当前的selector再找一个新的接受者,并不能做其他的改变。
NSString *result = [self performSelector:@selector(lowercaseString)];
我们来调用一下lowercaseString
方法,这个方法显然是NSString才有的方法。所以我们可以把它指派给一个NSString类型的对象。
- (id)forwardingTargetForSelector:(SEL)aSelector {
return @"APPLE";
}
这里将lowercaseString
方法找了个新的接受者,外界好像看起来什么都没有发生,但其实内部已经把接受者从self
变成了APPlE
对象。
消息的转发
还是上面那个例子,我们继续调用
[self performSelector:@selector(testForward:) withObject:@"arg1sdfsdfsdf"];
要使用消息的转发必须要覆盖两个方法在methodSignatureForSelector
和forwardInvocation
前者永辉为方法创建一个有效的签名。必须实现。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
[anInvocation setSelector:@selector(forwardTo:)];
NSString *arg1;
[anInvocation getArgument:&arg1 atIndex:2];
[anInvocation invokeWithTarget:self];
}
- (void)forwardTo:(NSString *)arg1 {
NSLog(@"%@",arg1);
}
输出
2015-08-21 15:23:37.560 objc_msgSendTest[18793:1974024] arg1sdfsdfsdf
这里我们把未实现的testForward
方法转发到了(void)forwardTo:(NSString *)arg1
方法上去
上面有一个小问题就是关于参数的问题,明明只有一个参数为什么Index为2呢,这是因为在objective-C
中的方法默认隐藏了两个参数,self
和_cmd
。这样说的话就很容易来解释方法签名中的"v@:@"
是什么鬼,v表示返回值void,接下来就是三个参数。
方法的缓存
这个里面有一个缓存机制,美团有一篇文章写得非常好,传送链在这里 深入理解Objective-C:方法缓存
文中可能会有错误,欢迎大牛们指正,以免误导别人。
网友评论