iOS消息传递

作者: HarriesChen | 来源:发表于2015-08-21 16:07 被阅读916次

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

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

    相关文章

      网友评论

        本文标题:iOS消息传递

        本文链接:https://www.haomeiwen.com/subject/ffmgqttx.html