iOS 消息转发机制

428 阅读7分钟

(一)对象的消息传递机制 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,但是只建议在线上环境的时候做处理,实际开发过程中还要把异常抛出来。

总结:

  1. 调用+ (BOOL)resolveInstanceMethod:(SEL)sel(或 + (BOOL)resolveClassMethod:(SEL)sel)方法,在此方法中添加相应selector以及IMP即可,允许用户在此时为该Class动态添加实现。如果有实现了,则调用并返回YES,那么重新开始objc_msgSend流程。对象会相应这个选择器,一般是因为它已经调用过class_addMethod。如果仍没实现,继续下面的步骤

  2. 调用- (id)forwardingTargetForSelector:(SEL)aSelector方法,尝试找到一个能相应该消息的对象。如果获取到,则直接把消息转发给它,返回非nil对象。否则返回 nil ,继续下面的动作。

  3. 调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法,尝试获得一个方法签名。如果能获取,则返回非nil:创建一个 NSlnvocation 并传给forwardInvocation:

    调用- (void)forwardInvocation:(NSInvocation *)anInvocation方法,将获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了,并返回非nil。如果获取不到,则直接调用4抛出异常。

  4. 调用- (void)doesNotRecognizeSelector:(SEL)aSelector,默认的实现是抛出异常。如果第3步没能获得一个方法签名,执行该步骤。

完整消息转发实例代码:

代码例子:

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里缓存一份。