4、消息转发机制:动态方法解析、备用接收者、完整消息转发

消息转发,说白了就是给对象一个“改过自新”的机会。

当你在代码里调了一个对象没有实现的方法,系统不会直接崩溃。它会先给你三次机会,让你想办法把这个消息处理掉。这三步,就是OC的消息转发机制。

我个人习惯把这三次机会比作“三堂会审”:

  • 第一堂:问问类自己,能不能动态加点方法?
  • 第二堂:问问其他对象,能不能帮忙处理?
  • 第三堂:把消息完整包装好,丢给其他对象处理。

下面我们一个一个来看。

4.1 动态方法解析

这是第一道防线。当Runtime发现对象没有实现某个方法时,它会先调用+resolveInstanceMethod:(实例方法)或+resolveClassMethod:(类方法)。

你可以在这里用class_addMethod动态添加一个实现。说白了,就是“临时抱佛脚”。

核心方法:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(someMethod)) {
        class_addMethod(self, sel, (IMP)dynamicMethodIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

void dynamicMethodIMP(id self, SEL _cmd) {
    NSLog(@"动态添加的方法被调用了");
}

我在项目中遇到过这样一个场景:某个数据模型需要支持KVC,但有些属性是动态生成的。我就在resolveInstanceMethod里动态添加了getter和setter。嗯,这样既保持了灵活性,又不用写一堆重复代码。

小技巧:动态方法解析只会在第一次调用时触发。一旦添加成功,后续调用就不会再走这个流程了。

4.2 备用接收者

如果第一关没通过,Runtime会进入第二关:-forwardingTargetForSelector:

这个方法允许你把消息转发给另一个对象。说白了,就是“找个替身”。

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(someMethod)) {
        return self.backupObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

你想想看,这个机制在代理模式中特别有用。比如你有一个复杂的对象,不想让它承担太多职责,就可以把某些方法转发给专门的处理器。

我曾经用这个机制实现过一个“消息分发器”。主对象只负责核心逻辑,UI事件、网络回调这些统统转发给对应的子模块。代码结构一下子就清晰了。

注意:备用接收者返回的对象不能是nilself,否则会陷入死循环。我曾经踩过这个坑,调试了半天才发现是返回了self

4.3 完整消息转发

前两关都失败了?别急,还有最后一关。

完整消息转发分为两步:

  1. 方法签名-methodSignatureForSelector:,告诉Runtime这个方法需要什么参数和返回值。
  2. 消息转发-forwardInvocation:,把完整的NSInvocation对象交给你处理。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(someMethod:)) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([self.backupObject respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:self.backupObject];
    } else {
        [super forwardInvocation:anInvocation];
    }
}

这一步的灵活性最大。你可以修改参数、改变返回值、甚至把消息发给多个对象。我曾在日志系统里用过这个机制——所有未处理的消息都会被记录到日志,方便调试。

避坑指南:我曾经在forwardInvocation里忘记调用[super forwardInvocation:],结果导致某些系统方法也丢失了。嗯,后来养成了习惯:不处理的消息一定要交给父类。

4.4 消息转发流程图

下面这张图,把整个流程串起来了:

消息转发三阶段流程图 第一阶段:动态方法解析 +resolveInstanceMethod: / +resolveClassMethod: → 可以动态添加方法实现 → 返回YES表示已处理,NO则进入下一阶段 第二阶段:备用接收者 -forwardingTargetForSelector: → 返回一个能处理该消息的对象 → 返回nil则进入下一阶段 第三阶段:完整消息转发 -methodSignatureForSelector: → 提供方法签名 -forwardInvocation: → 处理NSInvocation → 可以修改参数、改变目标、记录日志等

4.5 三者的选择策略

你可能会问:这三个阶段,我该用哪个?

我个人建议这样选:

阶段 适用场景 性能影响
动态方法解析 类本身需要动态添加方法,比如懒加载、KVC 低(只调用一次)
备用接收者 有明确的代理对象,转发逻辑简单 中(直接转发)
完整消息转发 需要修改消息内容,或转发给多个对象 高(创建NSInvocation)

性能提醒:完整消息转发会创建NSInvocation对象,开销较大。如果只是简单转发,优先用备用接收者。

4.6 实际项目中的应用

我记得在一个旧项目里,有个模块需要支持远程配置。某些方法在配置下发前不存在,配置下发后才动态添加。我就在resolveInstanceMethod里根据配置动态添加实现。嗯,这样既不用频繁发版,又能灵活调整功能。

还有一个场景:某个第三方库的代理方法特别多,我不想在主控制器里写一堆空实现。就用forwardingTargetForSelector把代理方法全部转发给一个专门的处理器。代码瞬间清爽了。

我的习惯:在调试阶段,我会在forwardInvocation里加个日志,记录所有未处理的消息。这样能快速发现哪些方法被遗漏了。

消息转发机制,说白了就是OC给开发者留的“后悔药”。用好它,能让你的代码更灵活、更健壮。但也要注意,过度依赖转发会让代码变得难以追踪。嗯,适度就好。


公众号:蓝海资料掘金营,微信deep3321