×
广告

iOS中的unrecognized selector sent to instance..

96
fmxccccc
2017.03.11 15:33* 字数 1559

我们都知道是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
Web note ad 1