iOS_Objective-C 消息发送(消息查找 及 消息转发)过程

2022-07-20 14:19:14 浏览数 (2)

文章目录

  • 一、OC中的消息
  • 二、消息查找
  • 三、消息转发
    • 1、动态方法解析
    • 2、备援接收者(receiver)
    • 3、完整的消息转发

一、OC中的消息

​ 在对象上调用方法是Objective-C中常使用的功能,用OC的术语来说,叫“传递消息”(pass a message)。消息有“名称”(name)或“选择子”(selector),可以接收参数,而且可能还有返回值。

​ C语言使用的是“静态绑定”(static binding),即在编译期就能决定运行时所应调用的函数。

​ OC使用的是“动态绑定”(dynamic binding),所要调用的函数直到运行时才能确定。给对象发送消息可以这样写:

代码语言:javascript复制
id returnValue = [someObject messageName:parameter];

​ 其中someObject叫做“接受者”(receiver),messageName叫做“选择子”(selector)。选择子与参数合起来称为“消息”(message)。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数:objc_msgSend,其“原型”(prototype)如下:

代码语言:javascript复制
// 返回值类型; 参数:接受者、选择子(SEL是选择子的类型)、n个参数
void objc_msgSend(id self, SEL cmd, ...)

​ 这是个“参数个数可变的函数”(variadic function),编译器会把刚才的例子转换如下:

代码语言:javascript复制
id returnValue = objc_msgSend(someObject,
                             @selector(messageName:),
                             parameter);

二、消息查找

​ objc_msgSend函数会依据接收这与选择子的类型来调用适当的方法。查找顺序如下:

  1. 在接受者所属类中搜寻其“方法列表”(list of methods),如果能找到与选择子名称相符的方法,就跳至其实现代码。
  2. 若找不到,则沿着类的继承体系继续向上查找,等找到合适的方法之后再跳转。OC中的继承体系如下:

。 类向上找至根类,根类再向上是元类。 若最终还是没找到相符的方法,那就就会执行“消息转发”(message forwarding)操作。

这么看来,想调用一个方法似乎需要很多步骤。所幸objc_msgSend会将匹配结果缓存在“快速映射表”(fast map)里面,每个类都有这样一块缓存,若稍候还向该类发送与选择子相同的消息,那么执行起来就很快了。


三、消息转发

消息转发机制流程图如下:

。 系统给了三次补救的机会。


1、动态方法解析

​ 对象/类 在接收到无法解读的消息后,首先将调用下列类方法:

代码语言:javascript复制
  (BOOL)resolveInstanceMethod:(SEL)selector; // 对象无法解读
  (BOOL)resolveClassMethod:(SEL)selector; // 类的无法解读

​ 该方法参数接收了无法响应的那个方法的选择子,返回值类型为BOOL:表示这个类能否新增一个实例方法用以处理该选择子。

​ 1. 若此步骤的returnValue为NO,这会进入下一步(备援接收者)。

​ 2. 若想成功响应的前提是:相关方法的实现代码已经写好,只等运行的时候动态插在类里。此方案常用来实现@dynamic属性。例如:

代码语言:javascript复制
void autoDictionarySetter(id self, SEL _cmd, id value);
id autoDictionaryGetter(id self, SEL _cmd);

  (BOOL)resolveInstanceMethod:(SEL)selector {
  NSString *selectorString = NSStringFromSelector(selector);
  if (/* selector is from a @dynamic property  */) {
    if ([selectorString hasPrefix:@"set"]) {
          class_addMethod(self,
                          selector,
                          (IMP)autoDictionarySetter,
                          "v@:@");
    } else {
          class_addMethod(self,
                          selector,
                          (IMP)autoDictionarySetter,
                          "v@:@");
    }
    return YES;
  }
  return [super resolveInstanceMethod:selector];
}

​ 我们也可以吞噬无法响应的选择子,为此方法添加log,方便debug哪个方法没有实现:

代码语言:javascript复制
/**
要动态绑定的方法
@param self 要绑定方法的对象
@param _cmd 方法信息
@param value 方法参数
*/
void dynamicMethodIMP(id self, SEL _cmd, id value) {
  NSString *sel = NSStringFromSelector(_cmd);
  NSLog(@"self = %@ _cmd = %@ value = %@", self, sel, value);
}
// 1. Method resolution:
  (BOOL)resolveInstanceMethod:(SEL)sel {
  // v表示返回类型是void、@表示id、:表示SEL、@表示方法的具体参数
  class_addMethod(self, sel, (IMP)dynamicMethodIMP, "v@:@"); 
  return [super resolveInstanceMethod:sel]; // 返回YES, 整个消息发送过程会重启
}
  (BOOL)resolveClassMethod:(SEL)sel {
  class_addMethod(self.class, sel, (IMP)dynamicMethodIMP, "v@:@");
  return [super resolveClassMethod:sel]; // 返回YES, 整个消息发送过程会重启
}

2、备援接收者(receiver)

​ 第二步会调用如下方法,当此方法返回备援接受者(不是self或nil)时,重启整个发送过程。

代码语言:javascript复制
- (id)forwardingTargetForSelector:(SEL)selector;

​ 可以利用此步骤通过“组合”(composition)的方式模拟出“多重继承”(multiple inheritance)的某些特性。在一个对象的内部,可能还有一系列其他对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回,这样的话,在外界看来,就好像是该对象亲自处理了这些消息似的。举了个实现此步骤的例子如下:

代码语言:javascript复制
// 2. Fast forwarding: 可以把消息转发给其他对象
- (id)forwardingTargetForSelector:(SEL)aSelector {
  NSString *method = NSStringFromSelector(aSelector);
  if ([method isEqualToString:@"eating"]) {
      Dog *dog = [[Dog alloc] init];
      return dog;
  }
  return nil;  // 返回的不是 nil or self, 整个消息发送过程会重启, 当然发送的对象会变成return的对象
}

​ 如果此步骤的返回nil,这会进入下一步。

3、完整的消息转发

​ 如果转发算法来到这一步的话,唯一能做的就是启用完整的消息转发机制了。会触发以下方法:

代码语言:javascript复制
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
  1. 如果此方法返回nil,则会触发doesNotRecognizeSelector:方法,导致crash:Unrecognized selector to XXX
代码语言:javascript复制
- (void)doesNotRecognizeSelector:(SEL)aSelector;
  1. 如果该方法返回了一个函数签名,那么系统会创建一个NSInvocation对象,把尚未处理的那条消息有关的全部细节都封于其中,此对象包含选择子、目标(target)及参数。然后触发如下方法,其参数就是此对象:
代码语言:javascript复制
- (void)forwardInvocation:(NSInvocation *)anInvocation;

​ 这个方法可以实现得很简单:只需改变调用目标,使消息在新目标上得以调用即可。举例实现如下:

代码语言:javascript复制
// 3. Normal forwarding: 会创建 NSInvocation 对象,开销较大
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
  NSString *methodName = NSStringFromSelector(aSelector);
  if ([methodName isEqualToString:@"eating"]) {
      return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
  }
  return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
  SEL sel = [anInvocation selector];
  Dog *dog = [[Dog alloc] init];
  if ([dog respondsToSelector:sel]) {
    [anInvocation invokeWithTarget:dog];
    return;
  }
  [super forwardInvocation:anInvocation];
}

​ 然而这样实现出来的方法与“备援接收者”方案所实现的方法等效,所以很少人采用这么简单的实现方式。比较有用的实现方式:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或是改换选择子,等等。

参考:《Effective Objective-C 2.0》

0 人点赞