美文网首页苹果之美iOS DeveloperiOS
iOS中的unrecognized selector sent

iOS中的unrecognized selector sent

作者: fmxccccc | 来源:发表于2017-03-11 15:33 被阅读2166次

    我们都知道是Objective-C是一门动态语言,只有在系统运行时(RunTime)才会根据函数的名称找的对应的函数来调用,我们通常这样[xxx doSomething]来调用一个不带参数的函数,那么在系统运行的时候就会被转换为objc_msgSend(xxx,@selector(doSomething))(xxx指接收消息的对象, @selector()是一个SEL方法选择器),如果是一个带参数的方法则会转换为
    objc_msgSend(xxx,@selector(doSomething), arg1, arg2, ...).由此可以看出每一个Objective-C的函数中其实都自带了self(这里的self指代接收对象)以及SEL(方法_cmd)

    在我们日常的开发中或多或少都会遇到"xxx unrecognized selector sent to instance 0x100....",这个异常信息,它通常是消息接收者找不到对应的@selector()方法.

    但其实在这个异常抛出之前,系统给了我们几步来挽救:

    · + (BOOL)resolveInstanceMethod:(SEL)sel; / + (BOOL)resolveClassMethod:(SEL)sel;
    · - (id)forwardingTargetForSelector:(SEL)aSelector;
    · - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
    · - (void)forwardInvocation:(NSInvocation *)anInvocation;
    

    其中在第一个方法中有两种方式分别对应该对象的InstanceClass,而第三个与第四个方法永远是成对出现的.他们的先后顺序就是1-4来执行的,总体分为三步1,2,(3-4),先来看看他们的一个总体流程:

    流程图(图片来自于网络) </br>

    整个的示例代码如下:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            // insert code here...
            NSLog(@"Hello, World!");
            DemoTest *test = [[DemoTest alloc] init];
            [test performSelector:@selector(demoTest)];
        }
        return 0;
    }
    
    @interface DemoTest : NSObject
    
    @end
    
    @implementation DemoTest
    
    @end
    
    

    我们在main函数中生成了一个DemoTest并且调用demoTest方法,但是从代码中可以看出DemoTest类中并没有我们要找的方法,那么如果运行程序就会报错,接下来我们来逐步分析系统提供给我们的补救方法,这些方法都是写在DemoTest中的:

    + (BOOL)resolveInstanceMethod:(SEL)sel; / + (BOOL)resolveClassMethod:(SEL)sel;

    这是整个流程中的第一步,sel参数是无法解析的方法名.在这个方法中我们可以动态的为消息的接收者添加这个sel

    @implementation DemoTest
    
    void demoTestMethod(id self, SEL _cmd)
    {
        NSLog(@"被调用...%@",NSStringFromSelector(_cmd));
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
        NSString *methodName = NSStringFromSelector(sel);
        
        if ([methodName isEqualToString:@"demoTest"])
        {
            class_addMethod([self class], sel, (IMP)demoTestMethod, "v@:");
            return YES;
        }
        
        return [super resolveInstanceMethod:sel];
    }
    
    @end
    

    我们首先获取这个sel的名字,再来判断这个方法是不是需要我们动态添加的那个方法,如果是需要动态添加的就调用

    OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
    

    方法来动态的为我们消息接收者添加方法:

    • cls指的是消息接收者.
    • name就是无法解析的方法名.
    • imp表示要添加的函数IMP指针(指向函数的具体实现).
    • types是添加函数的类型.

    这里需要解释下types:这个参数就是定义函数返回值类型与参数类型的字符串(如果有参数的话).举个官方文档的例子- (BOOL)containsString:(NSString *)str,转换为types就是:c@:@,其中

    • c 对应函数中的返回值(这里的返回值是BOOL),其余不同的返回值可以参考苹果官方文档,也可以通过打印@encode(type-name)来看看不同的返回值这里所对应的标识.
    • @ 对应消息的接收者(self)
    • : 对应SEL(_cmd)对象(containsString:)
    • @ 对应函数中的参数(str)

    这里其实第一步就走完了,如果在该函数内为指定的sel提供实现,无论返回YES还是NO,编译运行都是可以通过的,但如果在该函数内并不真正为sel提供实现,无论返回YES还是NO都会进入下一步.

    - (id)forwardingTargetForSelector:(SEL)aSelector;

    在第一步中如果接收者中没有实现对应的方法的话,就会进入这个函数,去寻找是否有别的对象可以接收这个消息,我们先创建一个新的类DemoObject,并且在这个类中增加- (void)demoTest;方法:

    @interface DemoObject : NSObject
    
    - (void)demoTest;
    
    @end
    
    @implementation DemoObject
    
    - (void)demoTest
    {
        NSLog(@"这是DemoObject中的方法");
    }
    
    @end
    

    接下来我们回到DemoTest类中:

    @implementation DemoTest
    
    - (id)forwardingTargetForSelector:(SEL)aSelector
    {
        NSString *methodName = NSStringFromSelector(aSelector);
        
        if ([methodName isEqualToString:@"demoTest"])
        {
            DemoObject *demoObject = [[DemoObject alloc] init];
            return demoObject;
        }
        
        return [super forwardingTargetForSelector:aSelector];
    }
    
    @end
    

    因为我们知道在DemoObject中是有aSelector方法的实现的,所以我们这里直接返回DemoObject对象,如果这个函数的返回值为nil的话,系统将继续进入到下一步.

    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector; - (void)forwardInvocation:(NSInvocation *)anInvocation;

    这两个函数都成对出现的,也就是说系统给我们提供的补救方法中一共分为三步(这就是开始为什么要分为1,2,(3-4)).我们先来看看第一个方法:

    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

    这个方法通过aSelector参数返回了一个NSMethodSignature对象,这个对象中包含一个方法中返回值与参数类型的信息,我们通常使用methodSignatureForSelector:方法来创建,或者在
    macOS 10.5以后的版本中我们可以使用signatureWithObjCTypes:方法来创建.我们可以使用methodReturnType属相来查看一个方法的返回值(更多).

    @implementation DemoTest
    
    -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    {
        NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
        
        if (!signature)
        {
            if([DemoObject instancesRespondToSelector:aSelector])
            {
                signature = [DemoObject instanceMethodSignatureForSelector:aSelector];
            }
        }
        
        return signature;
    }
    
    @end
    

    系统会根据这个signature创建一个NSInvocation对象作为参数传递给下一个方法

    - (void)forwardInvocation:(NSInvocation *)anInvocation

    在iOS中,有两种方式可以调用SEL,一个是performSelector:系列的函数,还有个就是NSInvocation.

    NSInvocation包含了一个消息中的所有信息,例如:接收对象,返回值,参数,SEL.我们也可以通过这个对象来进行消息的传递.

    @implementation DemoTest
    
    -(void)forwardInvocation:(NSInvocation *)anInvocation
    {
        if ([DemoObject instancesRespondToSelector:anInvocation.selector])
        {
            [anInvocation invokeWithTarget:[[DemoObject alloc] init]];
        }
    }
    
    @end
    

    我们通过anInvocation中的selector属性来判断DemoObject中是否包含这个方法.如果有的话就就调用DemoObject中的这个方法.

    补充

    在上面两个方法中我们都用到了instancesRespondToSelector:方法, 我们除了这个方法外其实还熟悉另外一个respondsToSelector:这两者都是用来判断某个方法是否存在,但区别在于:

    • instancesRespondToSelector:用于类去判断实例方法是否存在.
    • respondsToSelector:用于类判断类方法是否存在,实例判断实例方法是否存在.

    我们再来看看一个普通的NSInvocation是怎么工作的,这里搬一个网上找来的例子

    - (void)viewDidLoad {
        [super viewDidLoad];
        SEL myMethod = @selector(myLog);
        //创建一个函数签名,这个签名可以是任意的,但需要注意,签名函数的参数数量要和调用的一致。
        NSMethodSignature * sig  = [NSNumber instanceMethodSignatureForSelector:@selector(init)];
        //通过签名初始化
        NSInvocation * invocatin = [NSInvocation invocationWithMethodSignature:sig];
        //设置target
        [invocatin setTarget:self];
        //设置selecteor
        [invocatin setSelector:myMethod];
        //消息调用
        [invocatin invoke];
        
    }
    
    -(void)myLog{
        NSLog(@"MyLog");
    }
    

    上面这个是不带参数的函数的调用方法,那么我们来看看带参数的调用方法:

    - (void)viewDidLoad {
        [super viewDidLoad];
        SEL myMethod = @selector(myLog:parm:parm:);
        NSMethodSignature * sig  = [[self class] instanceMethodSignatureForSelector:myMethod];
        NSInvocation * invocatin = [NSInvocation invocationWithMethodSignature:sig];
        [invocatin setTarget:self];
        [invocatin setSelector:myMethod];
        int a=1;
        int b=2;
        int c=3;
        [invocatin setArgument:&a atIndex:2];
        [invocatin setArgument:&b atIndex:3];
        [invocatin setArgument:&c atIndex:4];
        [invocatin invoke];
    }
    
    -(void)myLog:(int)a parm:(int)b parm:(int)c{
        NSLog(@"MyLog%d:%d:%d",a,b,c);
    }
    

    这里要说明以下的是为什么setArgument:要从2开始,因为这个方法"翻译"成我们之前所说的types的时候就是v@:i:i:i前面的@与:都被占用了所以要2从开始.

    那么如果以上三步都还没有完成补救的话,系统就会调用doesNotRecognizeSelector:方法抛出异常了.

    相关文章

      网友评论

      本文标题:iOS中的unrecognized selector sent

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