iOS消息传递

在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"];

要使用消息的转发必须要覆盖两个方法在methodSignatureForSelectorforwardInvocation
前者永辉为方法创建一个有效的签名。必须实现。

- (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:方法缓存

文中可能会有错误,欢迎大牛们指正,以免误导别人。

推荐阅读更多精彩内容