iOS消息传递

在iOS开发中经常会遇到unrecognized selector sent to instance 0x100111df0'的问题,这是为什么呢,从字面上理解来说是无法识别的selector子发送给对象,其实调用一个不存在的方法就会遇到这个问题。
严格来说iOS中不存在方法调用的说法,应该说是消息的传递。

消息传递和函数调用的区别就是,你可以在任意的时候对一个对象发送任何消息,而不需要在编译的时候声明。但是函数调用就不行。

标题1

- (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,……)方法系统执行的步骤为:
0.判断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'的错误就来了。

image.png

以上是处理消息的流程图。这里可以看到查找一个方法需要经过很多的步骤,所以我们很多次机会来弥补这种错误,但是越往后面处理消息所消耗的代价越大。我们从第一步开始看,最好能够在一开始就找到相应的selector,那么他就会把方法缓存起来,等再次调用相同的方法的时候就会直接从缓存中取出来,那效率很高,和直接用c调用的速度慢不了多少。在没有缓存的情况下会从类的方法表里面进行查找。一个对象会有一个isa指针来指向自己所属的类。而类则会有一个方法表(dispatch table),用于将selector和真正实现的内存地址对应起来。另外还有一个指针会指向父类,这样就可以逐级向上查找直到基类。如下图

22.png

方法的动态解析

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

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

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

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

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,703评论 0 9
  • 在iOS开发中经常会遇到unrecognized selector sent to instance 0x1001...
    HarriesChen阅读 2,908评论 0 12
  • 消息发送和转发流程可以概括为:消息发送(Messaging)是 Runtime 通过 selector 快速查找 ...
    lylaut阅读 1,835评论 2 3
  • 我们常常会听说 Objective-C 是一门动态语言,那么这个「动态」表现在哪呢?我想最主要的表现就是 Obje...
    Ethan_Struggle阅读 2,190评论 0 7
  • 本文转载自:http://yulingtianxia.com/blog/2014/11/05/objective-...
    ant_flex阅读 757评论 0 1