消息机制
问题
OC 中方法调用的本质是什么?objc_msgSend 的查找过程和消息转发的三个阶段是什么?
答案
方法调用 = 消息发送
// OC 代码
[person sayHello];
// 编译后
objc_msgSend(person, @selector(sayHello));
// 带参数
[person sayHelloTo:@"Alice"];
// 编译为
objc_msgSend(person, @selector(sayHelloTo:), @"Alice");
消息查找流程
缓存查找(cache_t):
- 使用哈希表(散列表)存储最近调用过的方法
SEL → IMP映射,查找速度极快(O(1))- 缓存未命中才遍历方法列表
消息转发三阶段
当方法查找失败后,进入消息转发流程(而非直接崩溃):
阶段一:动态方法解析(Dynamic Resolution)
// 动态添加方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(eat)) {
// 动态添加 eat 方法
class_addMethod(self, sel,
(IMP)dynamicEat,
"v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void dynamicEat(id self, SEL _cmd) {
NSLog(@"动态添加的 eat 方法");
}
阶段二:快速转发(Fast Forwarding)
// 将消息转发给另一个对象处理
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(drive)) {
// 让 driver 对象处理 drive 消息
return self.driver;
}
return [super forwardingTargetForSelector:aSelector];
}
阶段三:完整转发(Normal Forwarding)
// 1. 提供方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(fly)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
// 2. 处理 Invocation(可以修改参数、目标、甚至不执行)
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;
if ([self.bird respondsToSelector:sel]) {
[anInvocation invokeWithTarget:self.bird];
} else {
[super forwardInvocation:anInvocation];
}
}
三个阶段的对比
| 阶段 | 方法 | 特点 | 使用场景 |
|---|---|---|---|
| 动态解析 | resolveInstanceMethod: | 最快,动态添加方法 | @dynamic 属性、CoreData |
| 快速转发 | forwardingTargetForSelector: | 较快,整个消息转给另一个对象 | 代理模式、组合模式 |
| 完整转发 | forwardInvocation: | 最灵活,可修改参数和多目标转发 | 多继承模拟、日志拦截 |
实际应用:Crash 防护
@implementation NSObject (CrashGuard)
- (NSMethodSignature *)guard_methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *sig = [self guard_methodSignatureForSelector:aSelector];
if (!sig) {
// 返回一个空签名,避免崩溃
sig = [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return sig;
}
- (void)guard_forwardInvocation:(NSInvocation *)anInvocation {
// 记录日志但不崩溃
NSLog(@"⚠️ Unrecognized selector: -[%@ %@]",
NSStringFromClass([self class]),
NSStringFromSelector(anInvocation.selector));
}
@end
常见面试问题
Q1: 为什么 OC 叫"消息发送"而不是"方法调用"?
答案:
C/C++ 的方法调用在编译时就确定了函数地址(静态绑定),而 OC 的方法调用在运行时通过 objc_msgSend 动态查找。消息可以被转发、拦截、甚至不被处理,这是消息发送与方法调用的本质区别。一个对象可以接收它没有实现的消息(通过消息转发)。
Q2: objc_msgSend 为什么用汇编实现?
答案:
- 性能:作为所有方法调用的入口,必须极致快。汇编可以跳过 C 函数调用的栈帧建立开销
- 参数透传:需要将任意类型、任意数量的参数透传给实际方法实现(IMP),C 无法做到
- 尾调用优化:可直接 jump 到 IMP,复用调用者的栈帧
Q3: [self class] 和 [super class] 返回什么?
答案:
@interface Son : Father
@end
@implementation Son
- (void)test {
NSLog(@"%@", [self class]); // Son
NSLog(@"%@", [super class]); // Son(不是 Father!)
}
@end
[super class] 并不是发送消息给父类对象,而是从父类的方法列表开始查找 class 方法。最终调用的是 NSObject 的 class 方法,返回的是消息接收者 self 的类——即 Son。
Q4: 能否向编译后的类中增加实例变量?向运行时创建的类呢?
答案:
- 已编译的类:❌ 不能添加实例变量。因为
class_ro_t(包含 ivars)在编译时确定,后续修改会破坏内存布局 - Runtime 创建的类(注册前):✅ 可以用
class_addIvar添加实例变量,但必须在objc_registerClassPair之前