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事件、网络回调这些统统转发给对应的子模块。代码结构一下子就清晰了。
注意:备用接收者返回的对象不能是nil或self,否则会陷入死循环。我曾经踩过这个坑,调试了半天才发现是返回了self。
4.3 完整消息转发
前两关都失败了?别急,还有最后一关。
完整消息转发分为两步:
- 方法签名:
-methodSignatureForSelector:,告诉Runtime这个方法需要什么参数和返回值。 - 消息转发:
-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 消息转发流程图
下面这张图,把整个流程串起来了:
4.5 三者的选择策略
你可能会问:这三个阶段,我该用哪个?
我个人建议这样选:
| 阶段 | 适用场景 | 性能影响 |
|---|---|---|
| 动态方法解析 | 类本身需要动态添加方法,比如懒加载、KVC | 低(只调用一次) |
| 备用接收者 | 有明确的代理对象,转发逻辑简单 | 中(直接转发) |
| 完整消息转发 | 需要修改消息内容,或转发给多个对象 | 高(创建NSInvocation) |
性能提醒:完整消息转发会创建NSInvocation对象,开销较大。如果只是简单转发,优先用备用接收者。
4.6 实际项目中的应用
我记得在一个旧项目里,有个模块需要支持远程配置。某些方法在配置下发前不存在,配置下发后才动态添加。我就在resolveInstanceMethod里根据配置动态添加实现。嗯,这样既不用频繁发版,又能灵活调整功能。
还有一个场景:某个第三方库的代理方法特别多,我不想在主控制器里写一堆空实现。就用forwardingTargetForSelector把代理方法全部转发给一个专门的处理器。代码瞬间清爽了。
我的习惯:在调试阶段,我会在forwardInvocation里加个日志,记录所有未处理的消息。这样能快速发现哪些方法被遗漏了。
消息转发机制,说白了就是OC给开发者留的“后悔药”。用好它,能让你的代码更灵活、更健壮。但也要注意,过度依赖转发会让代码变得难以追踪。嗯,适度就好。
公众号:蓝海资料掘金营,微信deep3321