iOS 消息转发机制

(一)对象的消息传递机制 objc_msgSend()

这叫做“给某个对象发送某条消息”。消息有“名称”或“选择子(selector)”之说。消息可以接受参数,而且还可以有返回值。

id returnValue = [someObject messgeName:parameter];

本例中,someObject叫做方法调用者,也叫做接受者(receiver)。messageName:是方法名,也叫做选择子(selector)。选择子与参数合起来叫做”消息“(message)。在运行时,编译器会把上面这个格式的方法调用转化为一条标准的C语言函数调用,该函数就是鼎鼎有名的objc_msgSend(),该函数是消息objc里在运行时传递机制中的核心函数,其原型如下:

void objc_msgSend(id self, SEL cmd, ...)
多个参数按照顺序排列

根据原型转换后:

id returnValue = objc_msgSend(someObject,  @selector(messageName:), parameter)

执行这条语句的时候,向一个对象发送消息时,首先会在对象类的cache,method list以及父类对象的cache,method list依次查找SEL对应的IMP
(cache文末有解释-方法缓存)

如果没有找到,并且实现了动态方法决议机制就会决议。如果没有实现动态决议机制或者决议失败且实现了消息转发机制。就会进入消息转发流程。否则程序Crash.

消息具体传递流程:

objc_msgSend()函数会依据接受者(调用方法的对象)的类型和选择子(方法名)来调用适当的方法。

  • 接收者会根据isa指针找到接收者自己所属的类,然后在所属类的”方法列表“(method list)中从上向下遍历。如果能找到与选择子名称相符的方法,就根据IMP指针跳转到方法的实现代码,调用这个方法的实现。
  • 如果找不到与选择子名称相符的方法,接收者会根据所属类的superClass指针,沿着类的继承体系继续向上查找(向父类查找),如果能找到与名称相符的方法,就根据IMP指针跳转到方法的实现代码,调用这个方法的实现。
  • 如果在继承体系中还是找不到与选择子相符的方法,此时就会执行”消息转发(message forwarding)“操作。

(二)消息转发流程

消息转发分为两个阶段。第一阶段叫做“动态方法解析(dynamic method resolution)”,或者叫“动态方法决议”。第二阶段涉及到“完整的消息转发机制(full forwarding mechanism)”,或者叫“完整的消息转发原理”。

  • 动态方法决议

字面上我的理解是:消息在发送过程中进行判断到底这消息该由谁接收

一:询问是否有动态添加方法来进行处理
Objective C 提供了一种名为动态方法决议的手段,使得我们可以在运行时动态地为一个 selector 提供实现。我们只要实现

+ (BOOL)resolveInstanceMethod:(SEL)selector;
+ (BOOL)resolveClassMethod:(SEL)selector;**

具体代码:

//People.m
void speak(id self, SEL _cmd){
    NSLog(@"Now I can speak.");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSLog(@"resolveInstanceMethod:  %@", NSStringFromSelector(sel));
    if (sel == @selector(speak)) {
        class_addMethod([self class], sel, (IMP)speak, "V@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

+ (BOOL)resolveClassMethod:(SEL)name  
{  
    NSLog(@" >> Class resolving %@", NSStringFromSelector(name));  
      
    return [super resolveClassMethod:name];  
}
  • 完整的消息转发

完整的消息转发又分为两个阶段,第一阶段称为备援接受者(replacement receiver),第二阶段才是启动完整的消息转发机制。

1.备援接收者

询问别人是否可以帮助实现一下

 (id)forwardingTargetForSelector:(SEL)selector;

具体代码:

- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"forwardingTargetForSelector:  %@", NSStringFromSelector(aSelector));
    Bird *bird = [[Bird alloc] init];
    if ([bird respondsToSelector:aSelector]) {
        return bird;
    }
    return [super forwardingTargetForSelector: aSelector];
}
// Bird.m
- (void)fly {
    NSLog(@"I am a bird, I can fly.");
}

方法参数代表未知的选择子,返回值为备援接受者,若当前接受者能找到备援接受者,就直接返回,这个未知的选择子将会交由备援接受者处理。如果找不到备援接受者,就返回nil,此时就会启用“完整的消息转发机制”。

2.完整的消息转发

- (void)forwardInvocation:(NSInvocation *)invocation;

核心实现代码:

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSLog(@"forwardInvocation: %@", NSStringFromSelector([anInvocation selector]));
    if ([anInvocation selector] == @selector(code)) {
       Monkey *monkey = [[Monkey alloc] init];
        [anInvocation invokeWithTarget:monkey];
    }   
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSLog(@"method signature for selector: %@", NSStringFromSelector(aSelector));
    if (aSelector == @selector(code)) {
        return [NSMethodSignature signatureWithObjCTypes:"V@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

看完核心代码还觉得慌,一脸雾水,具体怎么用呢?代码怎么写?写在哪里?一切都准备在后面,不要慌,继续往下看,待着疑问看下去

实现此方法时,如果发现调用操作不应该由本类处理,则需要沿着继承体系,调用父类的同名方法,这样一来,继承体系中的每个类都有机会处理这个调用请求,直至rootClass,也就是NSObject类。如果最后调用了NSObject的类方法,那么该方法还会继而调用”doesNotRecognizeSelector:“以抛出异常,此异常表明选择子最终也未能得到处理。消息转发到此结束。

最后消息未能处理的时候,还会调用到

- (void)doesNotRecognizeSelector:(SEL)aSelector

我们也可以在这个方法中做些文章,避免掉crash,但是只建议在线上环境的时候做处理,实际开发过程中还要把异常抛出来。

完整消息转发实例代码:

代码例子:

viewController中创建TestModel

    TestModel * model = [[TestModel alloc]init];
    [model testMethod];


TestModel.h

   -(void)testMethod;

TestModel.m

   -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    {
      if (aSelector == @selector(testMethod))
      {
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
       }
      return nil;
  }

-(void)forwardInvocation:(NSInvocation *)anInvocation
 {
    if (anInvocation.selector == @selector(testMethod))
    {
        TestModelHelper1 *h1 = [[TestModelHelper1 alloc] init];
        TestModelHelper2 *h2 = [[TestModelHelper2 alloc] init];
        [anInvocation invokeWithTarget:h1];
        [anInvocation invokeWithTarget:h2];
    }
 }



@implementation TestModelHelper1
-(void)testMethod
{
    NSLog(@"i am TestModelHelper1");
}
@end



@implementation TestModelHelper2
-(void)testMethod
{
    NSLog(@"i am TestModelHelper2");
}
@end

运行可看到消息转发日志

ps:方法缓存

通篇下来,发现调用一个方法并不像我们想的那么简单,更不像我们写的那么简单,一个方法的执行其实底层需要很多步骤。正因如此,objc_msgSend()会将调用过且匹配到的方法缓存在”快速映射表(fast map)“中,快速映射表就是方法的缓存表。每个类都有这样一个缓存。所以,即便子类实例从父类的方法列表中取过了某个对象方法,那么子类的方法缓存表中也会缓存父类的这个方法,下次调用这个方法,会优先去当前类(对象所属的类)的方法缓存表中查找这个方法,这样的好处是显而易见的,减少了漫长的方法查找过程,使得方法的调用更快。同样,如果父类实例对象调用了同样的方法,也会在父类的方法缓存表中缓存这个方法。

同理,如果用一个子类对象调用某个类方法,也会在子类的metaclass里缓存一份。而当用一个父类对象去调用那个类方法的时候,也会在父类的metaclass里缓存一份。

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