iOS中的unrecognized selector sent to instance..

我们都知道是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:方法抛出异常了.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,511评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,495评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,595评论 0 225
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,558评论 0 190
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,715评论 3 270
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,672评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,112评论 2 291
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,837评论 0 181
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,417评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,928评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,316评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,773评论 2 234
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,253评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,827评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,440评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,523评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,583评论 2 249

推荐阅读更多精彩内容