消息转发机制简述

563 阅读7分钟

前言

  在Objective-C中,如果某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法;在底层,所有方法都是普通的C语言函数,然而对象接收消息之后,究竟该调用哪个方法则完全取决于运行期决定,设置可以在程序运行时改变,这些特性使得Objective-C成为一门真正的动态语言。

发消息

给对象发送消息可以这样来写

id returnValue = [someObject messageName:param];

其原型如下

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

故编译器会将其转换为如下函数

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

  objc_msgSend函数会依据接收者(returnValue)与选择子(messageName)的类型来调用适当的方法;为了完成此操作,该方法需要在接收者所属的类中搜寻其方法列表,如果能找到与选择子名称相符的方法,就跳至其实现代码;若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转;若最终还是找不到相符的方法,那就执行消息转发操作。

消息调用过程还会存在如下边界情况
  a. objc_msgSend_stret:如果待发送的消息要返回结构体,那么可交由此函数处理。只有当CPU寄存器能够容纳得下消息返回类型时,这个函数才能处理此消息,否则,交由另一个函数执行派发。此时,那个函数会通过分配在栈上的某个变量来处理消息所返回的结构体
  b. objc_msgSend_fpret:如果消息返回的是浮点数,那么可交由此函数处理,在某些架构的CPU中调用函数时,需要对“浮点寄存器”做特殊处理,也就是说,通常所用的objc_msgSend在这种情况下并不合适。这个函数是为了处理x86等架构CPU中某些令人稍觉惊讶的奇怪情况
  c. objc_msgSendSuper:如果给超类发消息,例如[super message:parameter],那么就交由此函数处理。也有另外两个与objc_msgSend_stret和objc_msgSend_fpret等效的函数,用于处理发给super的相应消息

消息转发三部曲

  若想令类能理解某条消息,我们必须以程序码实现出对应的方法才行;但是,在编译期向类发送了其无法解读的消息并不会报错,因为在运行期可以继续向类中添加方法,所以编译器在编译时还无法确知类中到底会不会有某个方法实现(即编译器无法确定某类型对象到底能解读多少种选择子,因为运行期还可以向其中动态新增)。当对象接收到无法解读的消息后,就会启动“消息转发”机制,程序员可经由此过程告诉对象应如何处理未知消息

  • 消息转发分为两大阶段
      第一阶段先征询接收者,所属的类,看其能否动态添加方法,以处理当前这个“未知的选择子”,这叫做“动态方法解析
      第二阶段涉及“完整的消息转发机制”,如果运行期系统已经把第一阶段执行完了,那么接收者就无法再以动态新增方法的手段来相应包含该选择子的消息了。此时运行期系统会请求接收者以其他手段来处理与消息相关的方法调用。这又细分为两小步:
        1). 首先请接收者看看有没有其他对象能处理这条消息,若有,则运行期系统会把这条消息转给那个对象,于是消息转发过程结束,一切如常
        2). 若没有“备援接收者” ,则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息

第一步:动态方法解析

  对象接收到无法解读的消息后,首先将调用其所属类的以下方法(如果类无法立即响应某个选择子,那么就会启动消息转发流程)

// 未知选择子为实例方法
+ (BOOL)resolveInstanceMethod:(SEL)sel

// 未知选择子为类方法
+ (BOOL)resolveClassMethod:(SEL)sel
  • 使用这种方法的前提是:相关方法的实现代码已经写好,只等着运行时候动态插在类里面就可以了
  • 运行期添加方法时使用此函数

使用示例如下

.h
@property (nonatomic, strong) NSString *strTest;

.m
@dynamic strTest;

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    NSString *selectorString = NSStringFromSelector(sel);
    NSLog(@"selectorString ==> %@", selectorString);

    if ([selectorString hasPrefix:@"set"]) {
        // 所添加的方法使用纯C函数实现的
        // (IMP)autoautoDictionarySetter:函数指针,指向待添加的方法
        // "v@:@“:待添加方法的类型编码,本例中,开头字符表示方法的返回值类型,后续字符表示其所接受的各个参数
        // class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method))
        class_addMethod(self, sel, (IMP)autoautoDictionarySetter, "v@:@");  
    } else {
        class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
    }
    return YES;
}

id autoDictionaryGetter(id self, SEL _cmd) {
    return @"getter";
}

void autoautoDictionarySetter(id self, SEL _cmd, id value) {
    NSLog(@"Setter");
}

第二步:备援接收者

  假使动态方法解析中未能够处理未知选择子,接下来运行期系统会问接收者能不能把这条消息转给其他接收者来处理,即调用以下方法

// 返回可以处理未知选择子的对象(当返回非self\非nil时,消息被转给新对象执行)
- (id)forwardingTargetForSelector:(SEL)aSelector
  • 若想在发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制来做
  • 转发给另一个对象、改变方法时使用此函数

第三步:完整的消息转发

  如果转发算法来到完整的消息转发这一步的话,那么唯一能做的就是启用完整的消息转发机制了;首先创建NSInvocation对象,把尚未处理的那条消息有关的全部细节都封于其中;此对象包含选择子、目标及参数,再触发NSInvocation对象时,“消息派发系统”将亲自出马,把消息指派给目标对象,此时会调用以下函数

- (void)forwardInvocation:(NSInvocation *)anInvocation

  这个方法实现的很简单:只需修改调用目标,使消息在新目标上得以调用即可;然而这样实现出来的方法与“备援接收者”方案实现的方法等效,所以很少有人采用这么简单的实现方式;比较有用的实现方式为:在触发消息之前,先以某种方式改变消息内容,比如追加另外一个参数,或是改换选择子等等
  实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法;这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject;最后调用了NSObject类的方法,那么该方法还会继续调用“doesNotRecongizeSelector:”以抛出异常,此异常表明选择子最终未能得到处理

  • 需要转发给多个对象时使用此函数

为了更好的去理解整个过程可以参看以下此流程图


总结

  接收者在每一步中均有机会处理消息,步骤越往后,消息处理的代价就越大,最好能在第一步就处理完,这样的话,运行期系统就可以将此方法缓存起来,如果这个类的实例稍后还收到同名选择子,那么根本就无须启动消息转发流程。若想在第三步里将消息转给备援接收者,那还不如把转发操作提前到第二步,因为第三步只是修改了调用目标,这项改动放到第二步执行更为简单,不然的话,还得创建完整的NSInvocation

声明:本文参考自《Effective Objective C 2.0:编写高质量iOS与OS X代码的52个有效方法》


GitHub主页
CSDN Blog
Email:jinjob@icloud.com