消息机制和消息转发

1,消息机制
消息机制是OC动态性的体现, 相比于c语言的函数调用在编译的时候已经确定,在OC中每一个方法的实际调用需要等到运行时才能确定。 并且OC中调用方法都可以看做是向调用者发送了一条消息。消息有“名称”或“选择子(selector)”之说。消息可以接受参数,而且还可以有返回值。

例如 id object = [someObject messageName:parameter];
someObject叫做消息的接受者(方法的调用者)
messageName: 叫做方法名或者selector
parameter叫做参数
selector和参数组合起来叫做消息,调用方法就是向调用者发送一条消息。

消息调用到底是怎么执行的呢?其实OC经过编译后方法调用都会转化为下面的c语言函数调用

void objc_msgSend(id self, SEL cmd, ...)

这是一个可变参数的函数, 其中第一个参数是方法的调用者id类型,第二个参数SEL类型表示当前调用的方法名,之后是可变参数,如果方法需要参数后面会有相应的参数。

2,objc_msgSend消息派发的详细流程
objc_msgSend函数的作用就是根据self和SEL查找方法的IMP,然后调用,具体流程如下,(以调用一个对象的实例方法为例)

  • 首先判断receiver是否为nil, 如果为nil,直接return
  • 根据receiver的isa指针找到所属的类对象,在类对象的方法缓存cache中根据SEL查找IMP,如果查到到IMP,调用IMP,结束调用。如果没有找到,进行下一步。
  • 如果在类对象的方法缓存中没有命中SEL, 就会查找类对象的方法列表,如果命中SEL, 则调用IMP并且把SEL和IMP添加到类对象的方法缓存中然后结束调用。如果方法类表中没有查找到,进行下一步
  • 根据对象的superClass,到父类对象中重复上面两个步骤, 如果命中SEL,则把SEL和IMP缓存的类对象的方法缓存中,然后调用IMP结束调用。如果还是没有命中那就继续沿着superClass父链继续查找,知道superClass为nil。
  • 如果直到superClass为nil还是没有找到对应的SEL,就会走消息转发的流程

2,消息转发流程

消息转发大致分为三步: 动态方法解析、备用消息接受者和完整的消息转发。

(1)动态方法解析
其中动态方法解析类提供一个机会就是可以添加方法,前提是方法的实现已经写好。相关API如下:

处理没有实现的类方法
+ (BOOL)resolveClassMethod:(SEL)sel
处理没有实现的实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel 

这两个方法都是类方法,并且参数是SEL,返回值为BOOL类型表示是否添加方法的实现,默认返回NO表示没有添加, 如果添加了方法的实现可以返回YES

注意,动态方法解析时, 处理类方法和实例方法时,添加方法的时候添加方法的主体要搞清楚, 添加实例方法添加到类对象中,添加类方法添加到元类对象中

典型实现

+ (BOOL) resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically))
    {// 判断如果是需要处理的sel则添加实现
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    // 不能阻断父类的响应
    return [super resolveInstanceMethod:aSel];
}

(2) 备用接收者
当动态方法解析失败, 没有给相应的sel添加实现的时候, 就会进入消息转发流程, 而消息转发流程第一步就是备用接收者,可以返回一个对象,让对象去处理消息,如果返回的对象不为nil,消息派发系统将继续消息派发到新接受者,注意不要返回self,否则会陷入死循环
如果在子类中实现该方法, 注意如果没有添加处理时不要阻断父类的处理,需要调用父类的该方法
该方法可以在更昂贵的消息转发前重定向消息到指定的对象,如果转发的目标是捕获NSInvocation,或者在转发过程中操作参数或返回值,那么它是没有用的。

该方法是转发实例方法,以SEL为参数,返回值是id类型表示要转发的对象,默认返回nil, 
- (id)forwardingTargetForSelector:(SEL)aSelector

相应的也有转发类方法
+ (id)forwardingTargetForSelector:(SEL)aSelector

(3) 完整的消息转发

如果前两步都没有完成方法,就会进入完整的消息转发流程。首先会调用

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
或者
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector

该方法会根据SEL,返回一个方法描述的对象,如果指定的sel在当前实例或者类中没有找到,就返回nil (抛出异常crash),该方法用于创建NSInvocation对象。

// 上一步方法签名创建好之后系统会自动封装本次调用的方法信息,创建NSInvocation对象, 然后调用forwardInvocation:方法,最后一次机会进行处理。
- (void)forwardInvocation:(NSInvocation *)anInvocation

forwardInvocation:进行处理,这一步的处理可以直接转发给其它对象,即和第二步的效果等效,但是很少有人这么干,因为消息处理越靠后,就表示处理消息的成本越大,性能的开销就越大。所以,在这种方式下,会改变消息内容,比如增加参数,改变选择子等等。


代码如下
/// 完整的消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation{
   NSString * selStr = NSStringFromSelector(anInvocation.selector);
   if ([selStr isEqualToString:@"playPiano"]) {
       anInvocation.selector = NSSelectorFromString(@"travel:");
       /// 方法的第一个参数是self。第二个参数是sel。后面是参数
       NSString *paramStr = @"测试阿斯顿发";
       [anInvocation setArgument: ¶mStr atIndex:2];
       // 也可以设置其他对象调用
       [anInvocation invokeWithTarget:self];
       return;
   }
   
   [super forwardInvocation:anInvocation];
}


/// 返回一个方法的类型描述
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
   NSLog(@"%@",NSStringFromSelector(aSelector));
   NSString *method = NSStringFromSelector(aSelector);
   if ([@"playPiano" isEqualToString:method]) {
       
       NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
       return signature;
   }
   return nil;
}

如果在上述三步中都没有进行相应的处理,那么就会抛出异常crash并且报经典错误unrecognized selector sent to instance

你可能感兴趣的:(消息机制和消息转发)