05-消息转发
Objective-C 的消息发送与转发,是 Runtime 最核心的机制。理解它,就能理解 OC 为什么是动态语言,以及 `unrecognized selector` 崩溃背后到底发生了什么。
下面按 **发送 → 查找 → 解析 → 转发** 四个阶段拆解,每个阶段都配流程图。
---
## 全局流程图
```text
[obj doSomething]
│
│ 编译器改写
▼
objc_msgSend(obj, @selector(doSomething), ...)
│
▼
┌───────────────────────────────────────────────────────────────┐
│ Phase 1: 消息发送(快速路径) │
│ 查 cache → 查方法列表 → 查父类 → 找到 IMP → 直接调用 │
│ 找不到 → 进入 Phase 2 │
└───────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ Phase 2: 动态方法解析 │
│ +resolveInstanceMethod: / +resolveClassMethod: │
│ 动态添加 IMP → 重新走 Phase 1 │
│ 仍未解决 → 进入 Phase 3 │
└───────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ Phase 3: 快速转发 │
│ -forwardingTargetForSelector: │
│ 返回另一个对象 → objc_msgSend(新对象, sel) │
│ 返回 nil → 进入 Phase 4 │
└───────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ Phase 4: 标准转发 │
│ -methodSignatureForSelector: → NSMethodSignature │
│ -forwardInvocation: → NSInvocation → invoke │
│ 仍未处理 → doesNotRecognizeSelector: → crash │
└───────────────────────────────────────────────────────────────┘
```
---
## Phase 1:消息发送
### 1.1 编译器改写
你写的:
```objc
[obj doSomething];
```
编译器会改写成:
```objc
objc_msgSend(obj, @selector(doSomething));
```
带参数:
```objc
[obj doSomethingWith:a andB:b];
// ↓
objc_msgSend(obj, @selector(doSomethingWith:andB:), a, b);
```
`objc_msgSend` 是**汇编实现**,不是 C 函数。原因:
- 参数数量可变,C 函数无法完美转发所有参数和返回值。
- 需要尾调用优化(tail call),避免额外栈帧。
- 需要极致的快速路径性能。
### 1.2 快速路径流程图
```text
objc_msgSend(receiver, _cmd, ...)
│
▼
┌─────────────────────────────────────┐
│ Step 1: 判断 receiver 是否为 nil │
│ ├── nil → 直接返回 0/nil,不崩溃 │
│ └── 非 nil → 继续 │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Step 2: 获取 isa │
│ ├── 判断是否 Tagged Pointer │
│ │ └── 是 → 从指针值中提取类信息 │
│ └── 否 → 直接取 obj->isa │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Step 3: 查 cache_t │
│ cache_t 是哈希表,bucket_t 存 │
│ {SEL, IMP} │
│ ├── 命中 → tail call IMP,结束 │
│ └── 未命中 → 走慢速路径 │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Step 4: _class_lookupMethodAndLoadCache3 │
│ → lookUpImpOrForward │
└─────────────────────────────────────┘
```
### 1.3 慢速路径:lookUpImpOrForward
```text
lookUpImpOrForward(cls, sel, receiver, ...)
│
▼
┌─────────────────────────────────────┐
│ 1. 类是否已 realize? │
│ 否 → realizeClass(初始化类结构) │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 2. 遍历当前类的方法列表 │
│ class_rw_t → methods │
│ ├── 找到 → 缓存到 cache_t,返回 │
│ └── 未找到 → 继续 │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 3. 沿 superclass 链向上遍历 │
│ 每层都查方法列表 │
│ ├── 找到 → 缓存到当前类的 cache │
│ │ (注意:缓存在调用类) │
│ └── 到 NSObject 仍未找到 → 继续 │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 4. 动态方法解析 │
│ +resolveInstanceMethod: │
│ +resolveClassMethod: │
│ ├── 添加了 IMP → 重新查找,返回 │
│ └── 未添加 → 继续 │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 5. 返回 _objc_msgForward_impcache │
│ 即转发入口 │
└─────────────────────────────────────┘
```
**关键点**:
- 方法缓存 `cache_t` 是**每个类独立**的,子类调用父类方法,命中后会缓存到**子类自己的 cache** 中。
- `cache_t` 使用**散列表 + 开放寻址**,扩容时会丢弃旧缓存。
- `class_rw_t` 的方法列表是**排好序的**,查找时可以用二分,但 Runtime 实际上用线性遍历 + 缓存加速。
---
## Phase 2:动态方法解析
### 2.1 流程图
```text
lookUpImpOrForward 未找到 IMP
│
▼
┌───────────────────────────────────────────────────────────────┐
│ 调用 +resolveInstanceMethod: (实例方法) │
│ 或 +resolveClassMethod: (类方法) │
│ │
│ + (BOOL)resolveInstanceMethod:(SEL)sel { │
│ if (sel == @selector(doSomething)) { │
│ class_addMethod(self, sel, │
│ (IMP)dynamicMethod, │
│ "v@:"); │
│ return YES; │
│ } │
│ return [super resolveInstanceMethod:sel]; │
│ } │
└───────────────────────────────────────────────────────────────┘
│
├── 返回 YES 且 class_addMethod 成功
│ │
│ ▼
│ 重新走 lookUpImpOrForward → 找到 IMP → 调用
│
└── 返回 NO
│
▼
进入 Phase 3:快速转发
```
**深挖点**:
- `class_addMethod` 添加的方法会进入 `class_rw_t` 的方法列表,下次调用直接命中。
- 动态解析是**最后机会在「本对象」内部解决**,不是转发给别人。
- `@dynamic` 属性就是靠这个机制在运行时补 getter/setter。
- Core Data 的 `NSManagedObject` 大量使用动态解析。
---
## Phase 3:快速转发
### 3.1 流程图
```text
动态解析未解决
│
▼
┌───────────────────────────────────────────────────────────────┐
│ 调用 -forwardingTargetForSelector: │
│ │
│ - (id)forwardingTargetForSelector:(SEL)sel { │
│ if (sel == @selector(doSomething)) { │
│ return _helper; // 返回能处理该消息的对象 │
│ } │
│ return [super forwardingTargetForSelector:sel]; │
│ } │
└───────────────────────────────────────────────────────────────┘
│
├── 返回非 nil 对象
│ │
│ ▼
│ objc_msgSend(_helper, sel)
│ → 重新走完整消息发送流程
│ → 如果 _helper 也处理不了,它自己再走转发
│
└── 返回 nil / self
│
▼
进入 Phase 4:标准转发
```
**深挖点**:
- 返回 `self` 会导致**死循环**,因为 `self` 处理不了该消息。
- 快速转发**只转发消息,不改变返回值**,适合简单的多继承场景。
- 比标准转发快,因为它只调一次 `objc_msgSend`,不需要构造 `NSInvocation`。
- 典型应用:`NSProxy`、多继承模拟、消息路由。
---
## Phase 4:标准转发
### 4.1 流程图
```text
快速转发返回 nil
│
▼
┌───────────────────────────────────────────────────────────────┐
│ Step 1: 调用 -methodSignatureForSelector: │
│ │
│ - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { │
│ NSMethodSignature *sig = [super methodSignatureForSelector:sel]; │
│ if (!sig) { │
│ sig = [NSMethodSignature signatureWithObjCTypes:"v@:"]; │
│ } │
│ return sig; │
│ } │
│ │
│ ├── 返回 nil → 直接 doesNotRecognizeSelector: → crash │
│ └── 返回签名 → 继续 │
└───────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ Step 2: Runtime 用签名构造 NSInvocation │
│ 把 receiver、selector、参数、返回值全部打包进去 │
└───────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ Step 3: 调用 -forwardInvocation: │
│ │
│ - (void)forwardInvocation:(NSInvocation *)invocation { │
│ SEL sel = invocation.selector; │
│ if ([_target respondsToSelector:sel]) { │
│ [invocation invokeWithTarget:_target]; │
│ } else { │
│ [super forwardInvocation:invocation]; │
│ } │
│ } │
│ │
│ ├── 成功 invoke → 拿到返回值,结束 │
│ └── 未处理 → 继续 │
└───────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────┐
│ Step 4: 调用 -doesNotRecognizeSelector: │
│ → 抛出 NSInvalidArgumentException │
│ → unrecognized selector sent to instance 0x... │
│ → crash │
└───────────────────────────────────────────────────────────────┘
```
### 4.2 NSInvocation 的能力
`NSInvocation` 是标准转发的核心,它把一次消息调用**对象化**,可以:
- 修改 `target`
- 修改 `selector`
- 读取/修改任意参数
- 读取/设置返回值
- 延迟执行
- 多次 invoke
示例:实现多继承
```objc
- (void)forwardInvocation:(NSInvocation *)invocation {
SEL sel = invocation.selector;
if ([_objA respondsToSelector:sel]) {
[invocation invokeWithTarget:_objA];
} else if ([_objB respondsToSelector:sel]) {
[invocation invokeWithTarget:_objB];
} else {
[super forwardInvocation:invocation];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
NSMethodSignature *sig = [super methodSignatureForSelector:sel];
if (!sig) sig = [_objA methodSignatureForSelector:sel];
if (!sig) sig = [_objB methodSignatureForSelector:sel];
return sig;
}
```
### 4.3 深挖点
- `methodSignatureForSelector:` 必须返回**正确的类型编码**,否则 `NSInvocation` 解析参数会崩溃。
- `forwardInvocation:` 默认实现会调 `doesNotRecognizeSelector:`。
- 重写 `forwardInvocation:` 后,最好同时重写 `respondsToSelector:`,否则外部用 `respondsToSelector:` 判断会失败。
- `NSProxy` 是专门为转发设计的抽象类,比 `NSObject` 更适合做消息代理。
---
## 完整时序图
```text
调用者 Runtime 对象
│ │ │
│ objc_msgSend(obj,sel)│ │
│─────────────────────►│ │
│ │ 查 cache │
│ │───────┐ │
│ │◄──────┘ │
│ │ │
│ │ 未命中,查方法列表 │
│ │───────┐ │
│ │◄──────┘ │
│ │ │
│ │ 沿父类链查找 │
│ │───────┐ │
│ │◄──────┘ │
│ │ │
│ │ 仍未找到 │
│ │ resolveInstanceMethod │
│ │──────────────────────►│
│ │◄──────────────────────│
│ │ 返回 NO │
│ │ │
│ │ forwardingTargetForSelector │
│ │──────────────────────►│
│ │◄──────────────────────│
│ │ 返回 nil │
│ │ │
│ │ methodSignatureForSelector │
│ │──────────────────────►│
│ │◄──────────────────────│
│ │ 返回签名 │
│ │ │
│ │ 构造 NSInvocation │
│ │───────┐ │
│ │◄──────┘ │
│ │ │
│ │ forwardInvocation │
│ │──────────────────────►│
│ │◄──────────────────────│
│ │ 未处理 │
│ │ │
│ │ doesNotRecognizeSelector │
│ │──────────────────────►│
│ │◄──────────────────────│
│ │ 抛异常 → crash │
```
---
## 四阶段对比表
| 阶段 | 方法 | 时机 | 能否修改参数 | 性能 | 典型用途 |
|---|---|---|---|---|---|
| 消息发送 | `objc_msgSend` | 立即 | 否 | 最快 | 正常调用 |
| 动态解析 | `+resolveInstanceMethod:` | 首次调用时 | 否 | 快 | `@dynamic`、Core Data |
| 快速转发 | `-forwardingTargetForSelector:` | 解析失败后 | 否 | 较快 | 多继承、消息路由 |
| 标准转发 | `-methodSignatureForSelector:` + `-forwardInvocation:` | 快速转发失败后 | 是 | 慢 | AOP、NSProxy、多继承 |
---
## 面试高频深挖点
**1. objc_msgSend 为什么用汇编?**
参数可变、返回值类型不定、需要尾调用优化、快速路径不能有 C 栈开销。
**2. 方法缓存存在哪里?**
每个类的 `cache_t`,子类调用父类方法,命中后缓存在子类自己的 cache。
**3. 动态解析和转发的区别?**
动态解析是**给自己加方法**,转发是**把消息交给别人**。
**4. 快速转发和标准转发的区别?**
快速转发只调一次 `objc_msgSend`,不构造 `NSInvocation`,快但只能改 target;标准转发能改 target、selector、参数、返回值,慢但灵活。
**5. 为什么转发后要重写 respondsToSelector:?**
因为 Runtime 的转发不改变 `respondsToSelector:` 的返回值,外部判断会失败。
**6. unrecognized selector 崩溃发生在哪一步?**
`doesNotRecognizeSelector:`,即四阶段全部失败后。
**7. 如何防止 unrecognized selector 崩溃?**
重写 `forwardInvocation:` 兜底,或重写 `methodSignatureForSelector:` + `forwardInvocation:` 做统一处理,或给 `NSObject` 加分类做全局防护。
---
一句话总结:**OC 消息发送是「查缓存 → 查方法列表 → 查父类」的快速路径;找不到时依次走「动态解析 → 快速转发 → 标准转发 → 崩溃」四阶段,每一阶段都给开发者一次挽救机会,这就是 OC 动态性的核心体现。**
浙公网安备 33010602011771号