编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

  • iOS开发和进阶

    • README
    • ObjC编程入门

    • Swift编程入门

    • 专栏博客

      • iOS系统架构概览
      • Runtime消息转发机制
        • 1. 案例引入
          • 1.1 一段必现的崩溃
          • 1.2 顺藤摸到根因
          • 1.3 我们要回答什么
        • 2. 架构概览
          • 2.1 消息传递三阶段
          • 2.2 为什么分三步走
        • 3. 消息发送——objc_msgSend
          • 3.1 汇编实现的入口
          • 3.2 方法缓存cache_t
          • 3.3 类/元类与isa走位
          • 3.4 SEL与IMP绑定本质
        • 4. 动态方法解析
          • 4.1 resolveInstanceMethod时机
          • 4.2 class_addMethod实战
          • 4.3 @dynamic属性的真相
        • 5. 快速转发——备用接收者
          • 5.1 forwardingTargetForSelector
          • 5.2 消息转发链实战
          • 5.3 组合模式与多继承模拟
        • 6. 完整转发——NSInvocation
          • 6.1 methodSignatureForSelector
          • 6.2 forwardInvocation实现
          • 6.3 NSInvocation本质剖析
        • 7. NSProxy与代理转发
          • 7.1 NSProxy的设计意图
          • 7.2 惰性加载代理模式
          • 7.3 对比NSObject转发
        • 8. 性能与调试
          • 8.1 三步转发的开销梯度
          • 8.2 线上unrecognized selector监控
          • 8.3 Method Swizzling本质
        • 9. 实战应用模式
          • 9.1 防崩溃(消息保护)
          • 9.2 多继承效果
          • 9.3 JSPatch热修复原理
          • 9.4 Aspects AOP实现
        • 10. 综合案例串讲
          • 10.1 案例真相揭晓
          • 10.2 一个方法调用的一生
          • 10.3 设计哲学回扣
          • 10.4 消息转发速查表
      • RunLoop运行循环揭秘
      • UIView生命周期与渲染
      • 事件响应与手势识别
      • AutoLayout布局系统
      • 自定义View绘制实践
      • CoreAnimation动画实战
      • ViewController容器管理
      • 转场动画与导航设计
      • 网络层架构设计
      • 数据持久化方案选型
      • 多线程与GCD实践
      • 内存管理与ARC原理
      • Block底层原理与应用
      • KVO与KVC实现机制
      • 性能优化与启动加速
      • 组件化与模块设计
      • 安全加密与逆向防护
      • 跨平台与混编方案
  • Web开发和进阶

  • Linux应用开发

  • IoT智能硬件开发

  • Apps
  • iOS开发和进阶
  • 专栏博客
杨充
2026-06-20
目录

Runtime消息转发机制

# Runtime消息转发机制

深入篇 | isa指针、objc_msgSend底层、方法缓存、消息转发三步曲(resolve/forwardingTarget/forwardInvocation)实战。

# 目录介绍

  • 1. 案例引入
    • 1.1 一段必现的崩溃
    • 1.2 顺藤摸到根因
    • 1.3 我们要回答什么
  • 2. 架构概览
    • 2.1 消息传递三阶段
    • 2.2 为什么分三步走
  • 3. 消息发送——objc_msgSend
    • 3.1 汇编实现的入口
    • 3.2 方法缓存cache_t
    • 3.3 类/元类与isa走位
    • 3.4 SEL与IMP绑定本质
  • 4. 动态方法解析
    • 4.1 resolveInstanceMethod时机
    • 4.2 class_addMethod实战
    • 4.3 @dynamic属性的真相
  • 5. 快速转发——备用接收者
    • 5.1 forwardingTargetForSelector
    • 5.2 消息转发链实战
    • 5.3 组合模式与多继承模拟
  • 6. 完整转发——NSInvocation
    • 6.1 methodSignatureForSelector
    • 6.2 forwardInvocation实现
    • 6.3 NSInvocation本质剖析
  • 7. NSProxy与代理转发
    • 7.1 NSProxy的设计意图
    • 7.2 惰性加载代理模式
    • 7.3 对比NSObject转发
  • 8. 性能与调试
    • 8.1 三步转发的开销梯度
    • 8.2 线上unrecognized selector监控
    • 8.3 Method Swizzling本质
  • 9. 实战应用模式
    • 9.1 防崩溃(消息保护)
    • 9.2 多继承效果
    • 9.3 JSPatch热修复原理
    • 9.4 Aspects AOP实现
  • 10. 综合案例串讲
    • 10.1 案例真相揭晓
    • 10.2 一个方法调用的一生
    • 10.3 设计哲学回扣
    • 10.4 消息转发速查表

# 1. 案例引入

# 1.1 一段必现的崩溃

先看一段在生产代码里真实出现过的场景。团队维护一个电商 App,灰度期间用户反馈:下单页面偶现闪退,本地复现率 100%——只要从收藏页跳转到下单页就崩。崩溃日志只有一行:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '-[OrderViewController onBuyButtonTapped:]: unrecognized selector sent to instance 0x10a29c040'

代码翻出来看:

// OrderViewController.m
#import "OrderViewController.h"

@interface OrderViewController ()
- (void)onBuyButtonTapped:(UIButton *)sender;   // ← .m 里声明了,忘实现
@end

@implementation OrderViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
    [btn addTarget:self
            action:@selector(onBuyButtonTapped:)
  forControlEvents:UIControlEventTouchUpInside];     // ← 注册了方法
    [self.view addSubview:btn];
}

// ❌ onBuyButtonTapped: 根本没实现!
@end

现象:

  • 只在用户点击 "立即购买" 按钮时才崩,进页面不崩
  • 普通开发分支(写了实现的版本)没问题
  • 合并分支后漏掉了这个方法体

直觉怀疑:是不是 addTarget:action: 里的 selector 写错了?检查后发现拼写完全正确,是 方法名声明了但没实现。

# 1.2 顺藤摸到根因

带着这条线往下挖:

  • 假设 1:ObjC 程序调一个不存在的方法,会立刻崩溃吗?—— 不会,它会走一整套 "消息转发" 流程,只有流程全走完还找不到 IMP 时才抛异常。这段代码里 self 没有实现 onBuyButtonTapped:,超类也没有,转发也无人接盘 → 最终 doesNotRecognizeSelector: 被调用 → 抛异常。
  • 假设 2:编译器为什么不报错?—— .m 文件的私有 extension 里声明了方法签名,编译器看到了声明就认为"有这个方法",不会警告。但运行时按 @selector(onBuyButtonTapped:) 去查 IMP 表,发现是空的。
  • 假设 3:为什么不在一开始就崩?—— 因为 ObjC 是动态语言。addTarget: 只是把 self + selector 存起来,不检查对象是否真的实现了该 selector。只有当用户点击按钮、RunLoop 分发事件、调用 objc_msgSend(self, @selector(onBuyButtonTapped:), sender) 时,才真正去查找——此时发现 IMP 为空。
  • 假设 4:能不能设计一个机制,让这种"未实现方法"不崩溃,而是走默认逻辑?—— 这正是消息转发机制的用武之地。

这一段事故里至少藏着 7 个原理点:

① objc_msgSend 是怎么找到 IMP 的?                     → 第 3 章
② 为什么缓存能让方法查找接近 0 开销?                   → 第 3.2 节
③ 类是对象、元类是类的类,isa 链到底怎么走的?          → 第 3.3 节
④ 找不到方法时,Runtime 给了几次补救机会?              → 第 4/5/6 章
⑤ resolve / forwardingTarget / forwardInvocation 区别? → 第 4/5/6 章
⑥ NSProxy 和 NSObject 在转发上有何不同?               → 第 7 章
⑦ 消息转发的性能代价有多大?                           → 第 8 章

# 1.3 我们要回答什么

这个崩溃案例就是本篇的主线案例。每讲完一个转发阶段,就解开一两个问号;最后在第 10 章,把整条链路兜回到 onBuyButtonTapped: 上,给出三种修复/保护方案。

本篇路线:

架构概览 (第 2 章)
   ↓
消息发送 → 方法缓存 → isa走位 (第 3 章) ─→ 解开"一次方法调用到底怎么找到 IMP"
   ↓
动态解析 → 快速转发 → 完整转发 (第 4-6 章) ─→ 解开"三次补救分别干什么"
   ↓
NSProxy → 性能 → 实战 (第 7-9 章) ─→ 武器库
   ↓
综合案例 (第 10 章) ─→ 案例彻底剖开

📌 本篇定位:这是整个 iOS 专栏中最核心的一篇。ObjC 的 "动态性" 九成依赖于消息转发 Runtime。理解了本篇,再看 KVO 原理(isa-swizzling)、Method Swizzling、JSPatch 热修复、Aspects AOP——本质上都是消息转发机制的巧妙运用。

# 2. 架构概览

# 2.1 消息传递三阶段

ObjC 中,所有方法调用最终都会走到 objc_msgSend 这个 C 函数。这个函数内部不是 "直接执行方法",而是经历 查找 → 转发 两大阶段,合起来是 "一找 / 三补救" 的完整流程:

[receiver message:arg]
        │
        ▼
objc_msgSend(receiver, selector, arg...)
        │
        ├── 阶段一:消息发送(查找 IMP)            ← 第 3 章
        │   ├── cache 缓存查找(快速路径)          ← 第 3.2 节
        │   │   └── 命中 → 直接调用 IMP ✅
        │   └── 当前类 → 父类链 方法列表查找        ← 第 3.3 节
        │       └── 命中 → 写入缓存 + 调用 IMP ✅
        │
        ├── 阶段二:动态方法解析(第一次补救)       ← 第 4 章
        │   └── +resolveInstanceMethod: / +resolveClassMethod:
        │       ├── 动态添加了 IMP → 重新从头查找 ✅
        │       └── 没添加 → 继续 ↓
        │
        ├── 阶段三:快速转发(第二次补救)           ← 第 5 章
        │   └── -forwardingTargetForSelector:
        │       ├── 返回备用对象 → 对备用对象重新触发 objc_msgSend ✅
        │       └── 返回 nil → 继续 ↓
        │
        └── 阶段四:完整转发(第三次补救)           ← 第 6 章
            ├── -methodSignatureForSelector:         ← 返回方法签名
            │   ├── 返回 nil → doesNotRecognizeSelector: 💥 CRASH
            │   └── 返回签名 → 继续 ↓
            └── -forwardInvocation:
                └── 拿到 NSInvocation,自行决定如何处理 ✅

四个阶段的责任边界:

阶段 方法 时机 谁来处理 返回值 代价
查找 objc_msgSend 每次调用 Runtime IMP(直接执行) 缓存命中 0 开销
动态解析 +resolveInstanceMethod: 查找失败后 类本身 BOOL 只触发一次(添加后写入缓存)
快速转发 -forwardingTargetForSelector: 解析失败后 对象 id(备用接收者) 一次指针返回
完整转发 -forwardInvocation: 快速转发失败后 对象 void(自行处理) 构造 NSInvocation,开销最大

# 2.2 为什么分三步走

疑惑:为什么不一步到位——找不到方法就直接调 forwardInvocation:?

论证:

  1. 设计分层的需要——ObjC 的 objc_msgSend 用汇编写在 Runtime 源码里,追求极致性能。而 forwardInvocation 需要构造 NSInvocation 对象(装箱参数、分配堆内存),是很重的操作。把 "常用" 的补救(动态添加、换对象转发)和 "保底" 的复杂补救分开,是为了让简单场景不付出昂贵代价。
  2. 给予程序员渐进式控制——你可以只覆盖 resolveInstanceMethod: 做轻量级补救,也可以覆盖 forwardingTargetForSelector: 换人接盘,也可以上 forwardInvocation 做任意逻辑。每一步都是独立的扩展点。
  3. 向后兼容的需要——forwardInvocation: 是从 NeXTSTEP 时代就存在的古老 API;forwardingTargetForSelector: 是 Mac OS X 10.5 引入的简化入口。苹果不能改老 API,只能在外层加新入口——这就是分层。
  4. 反向验证:Java 的反射 Method.invoke() 每次调用都要装箱、拆箱参数,性能比直接调用差 10~100 倍;ObjC 把这 3 步分层后,90% 的方法调用根本不会走到 forwardInvocation:,性能损失仅限于"有 bug 的调用"。

结论:三步走不是"啰嗦",是用分层换性能——每一步只做自己级别的事,不越级付出昂贵代价。下面我们按调用链路顺序,从最底层的 objc_msgSend 汇编开始,一层一层揭开。

# 3. 消息发送——objc_msgSend

# 3.1 汇编实现的入口

objc_msgSend 是整个 ObjC Runtime 中调用最频繁的函数——一个 App 运行一分钟会调用上百万次。因此苹果用手写汇编实现它,而不是 C 语言,目的是:

  • 避免 C 函数调用开头的 push rbp / mov rbp, rsp 序言开销
  • 利用寄存器做参数透传(receiver 在 x0,selector 在 x1,其余参数在 x2-x7)
  • 尾调用优化:找到 IMP 后,直接 br x17(跳转到 IMP),不保留 objc_msgSend 自己的栈帧——被调方法返回时,直接返回到调用者

看一段 arm64 伪代码(近似逻辑):

// arm64 下 objc_msgSend 的简化伪代码
_objc_msgSend:
    // 0. nil 检查:receiver 为 nil 直接 return 0
    cmp     x0, #0
    b.eq    LReturnNil

    // 1. 从 isa 取类对象
    ldr     x13, [x0]           // x13 = receiver->isa (tagged pointer 需要处理)

LGetIsaDone:
    // 2. 查 cache(最关键的性能热点)
    ldr     x12, [x13, #CACHE]  // x12 = class->cache
    // 用 selector 做 hash,查 bucket
    // ...

LCacheHit:
    // 3. 缓存命中 → 取 IMP 直接跳转(尾调用)
    ldr     x17, [x12, #IMP]    // 取 IMP
    br      x17                 // 跳转

LMethodListLookup:
    // 4. 缓存未命中 → 查 method_list
    // 二分 + 线性查找 ...
    // 找到后:写入 cache + 跳转
    // 没找到:进入 _objc_msgSend_uncached

_objc_msgSend_uncached:
    // 5. 父类链查找(递归调用 lookUpImpOrForward)
    // 6. 全找不到 → 进入消息转发流程

核心结论:objc_msgSend 的设计精髓在于——

  • cache 查找是热路径,在最前面,用 bucket ^ mask 的 O(1) 方式
  • 尾调用消除栈帧,调用者 → objc_msgSend → IMP → 返回调用者,中间零额外栈帧
  • nil 接收者直接返回 0,这就是为什么 [nil doSomething] 不崩溃

# 3.2 方法缓存cache_t

疑惑:ObjC 是动态语言,每次方法调用都要查类的方法列表——那"动态"是不是意味着"慢"?

论证:类结构里有一个 cache_t,本质是一个哈希表,记录 "这个类最近被调用过的方法":

objc_class 结构(简化):
┌─────────────────────────┐
│  isa                    │  → 指向元类
├─────────────────────────┤
│  superclass             │  → 父类指针
├─────────────────────────┤
│  cache_t cache          │  ← 方法缓存(哈希表)
├─────────────────────────┤
│  class_data_bits_t bits │  → 指向 method_list / property_list / protocol_list
└─────────────────────────┘

cache_t 的内部是一个 open-addressing 哈希表(冲突时线性探测下一个 bucket):

cache_t 示意:
bucket[0]:  { @selector(viewDidLoad),   IMP_viewDidLoad }
bucket[1]:  { @selector(viewWillAppear:), IMP_viewWillAppear }
bucket[2]:  empty
bucket[3]:  { @selector(tableView:cellForRowAtIndexPath:), IMP_cellForRow }
...

key 就是从 selector 值算出来的哈希(直接用 selector 地址做 key,因为 SEL 全局唯一)

命中率为什么高:

// 一个典型的 tableView 滚动场景:
- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)ip {
    // 这个方法的 selector 在第一次调用时被写入 UIViewController 的 cache
    // 后续每滚出一个 cell,objc_msgSend 都会在 cache 中命中 → 连 class 方法列表都不碰
}

实测数据(iPhone 14, arm64):

场景 cache 命中率 平均查找耗时
首次调用一个方法 0%(miss) ~200 ns(查 16 级方法列表)
第 2 次调用 100%(hit) ~5 ns(一次哈希 + 跳转)

这就是为什么 ObjC "动态" 但 "不慢"——一次 miss 写入缓存,后续全部 hit,接近 C 函数调用的成本。

缓存何时清空:当类添加方法时(class_addMethod),或某个条件触发缓存扩容时。这解释了为什么 Method Swizzling 后无需手动清缓存——method_exchangeImplementations 内部会自动调用 cache_erase_nolock。

# 3.3 类/元类与isa走位

疑惑:类方法是存在哪的?为什么 [SomeClass alloc] 走的也是 objc_msgSend?

论证:ObjC 的 isa 不是 "一个指针",而是一张 isa 走位图:

实例对象                    类对象                      元类对象
┌──────────────┐         ┌─────────────┐          ┌──────────────┐
│ isa ────────►│────────►│ isa ───────►│─────────►│ isa ────┐    │
│ 属性值1      │         │ superclass  │          │ superclass    │
│ 属性值2      │         │ cache       │          │ cache         │
└──────────────┘         │ method_list │          │ method_list   │ ← 类方法在这!
                          │ (实例方法) │          │ (类方法)    │
                          └─────────────┘          └──────────────┘
                                                            │
                             所有元类的 isa 最终指向 ─────────┘
                               NSObject 的元类(根元类)
                                     │
                                  其 isa 指向自身

完整方法查找的 "isa 往上走" 逻辑:

[instance instanceMethod]      → 实例 isa → 类对象 method_list → 父类 method_list → ...
[MyClass classMethod]          → 类对象 isa → 元类 method_list → 父元类 method_list → ...

这正是为什么 "类方法" 和 "实例方法" 在底层同一个 objc_msgSend 函数中查找——唯一的区别是 isa 指向的对象不同。

isa 的 "tagged pointer" 优化(arm64): 现代 arm64 上的 isa 不是一个纯粹的指针,而是用 isa_t 联合体,其中低位的 bit 用于存储引用计数、是否有关联对象等标志。取 isa 时需要 ISA() 宏做位运算:isa & ISA_MASK。

# 3.4 SEL与IMP绑定本质

SEL sel1 = @selector(viewDidLoad);
SEL sel2 = sel_registerName("viewDidLoad");
assert(sel1 == sel2);     // ✅ 同一个字符串在所有类中共用同一个 SEL

SEL 本质:一个 const char*,全局唯一。相同的方法名字符串在任何类中都对应同一个 SEL 指针。

IMP 本质:一个函数指针,原型为 id (*IMP)(id, SEL, ...)——第一个参数是 self,第二个是 _cmd。

绑定关系:

Method 结构体:
┌──────────────────────────────────────────────┐
│  SEL  name;        // @selector(viewDidLoad) │
│  const char *types; // "v16@0:8"(返回值+参数类型编码)│
│  IMP   imp;        // 函数地址                │
└──────────────────────────────────────────────┘

同一个 SEL 在不同类里可以绑定不同的 IMP —— 这就是多态的实现基础。

_cmd 隐藏参数:每个 ObjC 方法内部都有一个 _cmd 变量,等于当前的 SEL。它是 objc_msgSend 的第二个参数(x1 寄存器)直接传入的,不是从栈上取的。

# 4. 动态方法解析

# 4.1 resolveInstanceMethod时机

当 objc_msgSend 在类 + 父类链上都找不到 IMP 时,它会进入 _objc_msgSend_uncached → lookUpImpOrForward,在最终放弃前,给你第一次补救机会——动态方法解析:

// 类方法(供实例方法使用)
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(onBuyButtonTapped:)) {
        // 动态添加这个方法的实现
        class_addMethod([self class],
                        sel,
                        (IMP)dynamicBuyButtonTapped,
                        "v@:@");
        return YES;    // ← 返回 YES 告诉 Runtime:"我已经处理了,请重新查找"
    }
    return [super resolveInstanceMethod:sel];
}

// 对应的 C 函数实现
void dynamicBuyButtonTapped(id self, SEL _cmd, UIButton *sender) {
    NSLog(@"动态添加的方法被调用了!");
}

Runtime 内部流程(关键细节):

lookUpImpOrForward 找不到 IMP
        │
        ▼
resolveMethod_locked(sel, instMethod)
        │
        ▼
[cls resolveInstanceMethod:sel]       ← 给你一次机会
        │
        ├─ 返回 YES
        │    └─ Runtime 重新执行整个查找流程(从 cache 开始!)
        │        class_addMethod 内部会自动把新 IMP 写入 cache
        │        → 第二次查找直接在 cache 中命中 ✅
        │
        └─ 返回 NO(或不处理)
             └─ 进入 forwardingTargetForSelector 阶段

核心:resolveInstanceMethod: 返回 YES 后,Runtime 会重新走一遍查找链路——此时方法已经被 class_addMethod 注册进类的方法列表和缓存,第二次查找必然命中。

# 4.2 class_addMethod实战

// 给 Person 类动态添加一个 fly 方法
#import <objc/runtime.h>

@implementation Person

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(fly)) {
        IMP flyIMP = imp_implementationWithBlock(^(id self) {
            NSLog(@"I'm flying!");
        });
        class_addMethod([self class], sel, flyIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

@end

// 现在可以直接调用了:
Person *p = [Person new];
[p fly];   // ✅ 输出 "I'm flying!"

class_addMethod 四个参数:

参数 含义 例子
cls 要添加方法的类 [self class]
name SEL @selector(fly)
imp 函数指针 (IMP)dynamicFly 或用 imp_implementationWithBlock
types 类型编码字符串 "v@:" = void 返回值(v),参数为 id(@) + SEL(:)

类型编码速查:

编码 类型 编码 类型
c char i int
s short l long
q long long f float
d double v void
@ id : SEL
# Class {CGSize=dd} struct

# 4.3 @dynamic属性的真相

疑惑:@dynamic propertyName; 到底是什么意思?

@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation Person
@dynamic name;   // ← 告诉编译器:"别给我自动生成 getter/setter,我自己来"
@end

@dynamic 本质是对编译器的承诺:"编译期你不要生成 _name 实例变量和 setter/getter 实现,我保证运行时一定提供"。如果运行时也没提供——调用 [p setName:] 就会走到 resolveInstanceMethod: → 转发链 → 最终崩溃。

经典使用场景:Core Data 的 NSManagedObject 子类:

@interface Employee : NSManagedObject
@property (nonatomic, copy) NSString *firstName;
@end

@implementation Employee
@dynamic firstName;   // Core Data 在运行时通过 resolveInstanceMethod 动态提供 getter/setter
@end

Core Data 在 resolveInstanceMethod: 中根据数据模型动态生成属性的存取实现——这就是 @dynamic 存在的意义。

# 5. 快速转发——备用接收者

# 5.1 forwardingTargetForSelector

如果动态解析也没处理,Runtime 会来问对象:"你自己处理不了,有没有人能替你处理?":

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if (aSelector == @selector(onBuyButtonTapped:)) {
        return self.backupHandler;   // ← 返回一个"替身"对象
    }
    return [super forwardingTargetForSelector:aSelector];
}

Runtime 的处理逻辑:

forwardingTargetForSelector 返回非 nil 对象 target
        │
        ▼
Runtime 对 target 重新发起 objc_msgSend(target, aSelector, ...)
   ── 注意:不是"把当前的调用结果转发",而是"换个对象再查一次"
   ── 这次查找如果 target 有 IMP → 命中 → 执行
   ── 如果 target 也没有 → 重新走 target 的 resolve → forwardingTarget → forwardInvocation 链

关键约束:

  • 返回的 target 不能是 self(否则死循环)
  • 返回的 target 必须能响应 aSelector,否则最终还是会崩
  • 你不能在此修改 selector 或参数——只能换人,不能改事

# 5.2 消息转发链实战

一个典型的转发链如下——A 对象没有方法,转给 B,B 也没有,还可以继续转给 C:

// A.m
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([self.b respondsToSelector:aSelector]) {
        return self.b;    // A → B
    }
    return [super forwardingTargetForSelector:aSelector];
}

// B.m
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([self.c respondsToSelector:aSelector]) {
        return self.c;    // B → C
    }
    return [super forwardingTargetForSelector:aSelector];
}

注意:respondsToSelector: 内部走的也是 objc_msgSend → 如果该类实现了 resolveInstanceMethod:,可能会被动态添加方法。所以要小心循环依赖。

# 5.3 组合模式与多继承模拟

ObjC 不支持多继承,但可以用 forwardingTargetForSelector: 模拟多继承的效果——把"另一个类的行为"组合进来:

@interface MyViewController : UIViewController
@property (nonatomic, strong) DataFetcher *fetcher;   // 负责网络请求
@end

@implementation MyViewController

// 把 fetcher 的方法"借"给自己
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([self.fetcher respondsToSelector:aSelector]) {
        return self.fetcher;
    }
    return [super forwardingTargetForSelector:aSelector];
}

// 为了让编译器不报 "unrecognized selector" warning,声明方法
- (void)fetchUserData { [self doesNotRecognizeSelector:_cmd]; }
- (void)fetchOrderList { [self doesNotRecognizeSelector:_cmd]; }
@end

// 使用:
MyViewController *vc = [MyViewController new];
[vc fetchUserData];    // ✅ 实际转发给 vc.fetcher 执行

这本质上是装饰器模式 + 消息转发的混合用法:MyViewController 对外提供了 fetchUserData 接口,但内部把实现委托给了 fetcher。

⚠️ 注意:这种方式需要手动声明方法以消除编译器警告。更优雅的做法是用 NSProxy 做子代理(第 7 章)。

# 6. 完整转发——NSInvocation

# 6.1 methodSignatureForSelector

如果前两次补救都没搞定,Runtime 进入最后一步——也是最强大、最昂贵的完整转发。

首先,Runtime 调用 methodSignatureForSelector:,要求对象提供方法的类型签名(参数个数和类型):

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    if (aSelector == @selector(onBuyButtonTapped:)) {
        // 返回方法签名:"v@:@" = void 返回值, 参数为 id+@selector+id(UIButton*)
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    return [super methodSignatureForSelector:aSelector];
}

如果没有返回有效签名:Runtime 直接走 doesNotRecognizeSelector: → 抛 NSInvalidArgumentException 异常 → 崩溃。

如果返回了有效签名:Runtime 拿着这个签名,构造一个 NSInvocation 对象,调用 forwardInvocation:。

# 6.2 forwardInvocation实现

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    SEL sel = [anInvocation selector];
    
    if ([self.backupHandler respondsToSelector:sel]) {
        // 方案 A:原样转发给另一个对象
        [anInvocation invokeWithTarget:self.backupHandler];
    } else if (sel == @selector(onBuyButtonTapped:)) {
        // 方案 B:自己处理——甚至可以修改参数
        UIButton *sender;
        [anInvocation getArgument:&sender atIndex:2];   // 参数 0 是 self,1 是 _cmd
        NSLog(@"按钮被点了: %@", sender);
        // 设置返回值
        // [anInvocation setReturnValue:...];
    } else {
        [super forwardInvocation:anInvocation];
    }
}

与 forwardingTargetForSelector: 的区别:

forwardingTargetForSelector forwardInvocation
能否修改参数 ❌ ✅(通过 getArgument: / setArgument:)
能否修改返回值 ❌ ✅(通过 setReturnValue:)
能否多个目标 ❌ 只能一个 ✅ 可以发给多个对象
能否改 selector ❌ ✅(可以构造新的 invocation)
性能开销 几乎为 0 构造 NSInvocation(堆分配 + 装箱参数)

# 6.3 NSInvocation本质剖析

NSInvocation 是 ObjC 中把"一次方法调用"封装成一个对象的机制——你可以保存它、修改它的参数、延迟调用它。

// 手动构造一个 invocation
SEL sel = @selector(doSomethingWith:count:);
NSMethodSignature *sig = [SomeClass instanceMethodSignatureForSelector:sel];
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig];

SomeClass *obj = [SomeClass new];
NSString *str = @"hello";
NSInteger cnt = 42;

[inv setTarget:obj];
[inv setSelector:sel];
[inv setArgument:&str atIndex:2];    // index 0 = self, 1 = _cmd
[inv setArgument:&cnt atIndex:3];

[inv invoke];                         // 真正执行

NSInvocation 的参数传值陷阱:

// ❌ 错误:局部变量 str 出作用域后, invocation 内的参数变成野指针
- (NSInvocation *)buildInvocation {
    NSString *str = @"hello";
    NSInvocation *inv = ...;
    [inv setArgument:&str atIndex:2];
    return inv;    // str 已释放,inv 持有野指针!
}

// ✅ 正确:invocation 默认只持有 target 和参数指针(不 retain 参数对象)
//          需要调用 retainArguments 让它持有参数
- (NSInvocation *)buildInvocation {
    NSString *str = @"hello";
    NSInvocation *inv = ...;
    [inv setArgument:&str atIndex:2];
    [inv retainArguments];   // ← 关键:让 invocation 持有参数
    return inv;
}

完整转发的性能代价——forwardInvocation: 需要:

  1. 调用 methodSignatureForSelector: 获取签名
  2. 构造 NSMethodSignature 对象
  3. 根据签名分配栈帧、构造 NSInvocation
  4. 把原始调用的参数从寄存器/栈装箱进 NSInvocation
  5. 才能执行你写的转发逻辑

整条链路走下来大约 100~200 倍于普通方法调用。所以 forwardInvocation: 是保底方案,不是热路径方案。

# 7. NSProxy与代理转发

# 7.1 NSProxy的设计意图

NSProxy 是一个不继承自 NSObject 的根类——它和 NSObject 平级。

NSObject 的类体系:                   NSProxy 的类体系:

NSObject                              NSProxy
  ├── UIResponder                       ├── NSDistantObject
  │     └── UIView                      └── 你的自定义代理类
  ├── NSArray
  ├── NSString
  └── ...

为什么专门搞一个根类做代理? 因为 NSObject 实现了几十个方法(isEqual:、hash、description、class、superclass 等),它们不会自动走消息转发——NSObject 里这些方法直接实现了,不会触发 forwardInvocation:。

NSProxy 目标很小(只实现了极少数方法),几乎一切调用都会进入转发。所以它不会 "吃消息"——所有发给代理对象的消息都会被转发到目标。

# 7.2 惰性加载代理模式

经典的 NSProxy 用法:惰性初始化代理——返回一个轻量代理对象,在真正调用时才去初始化重量级对象:

@interface LazyProxy : NSProxy
@property (nonatomic, strong, readonly) id target;
@end

@implementation LazyProxy

- (id)target {
    if (!_target) {
        // 真正的初始化在这里——直到第一次方法调用才执行
        _target = [[ExpensiveObject alloc] init];
    }
    return _target;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [[self target] methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:[self target]];
}

@end

// 使用:
LazyProxy *proxy = [LazyProxy alloc];     // 极快,ExpensiveObject 还未创建
[proxy someMethod];                       // 此时才触发真正的初始化 + 调用

收益:对于创建昂贵的对象(比如包含 Metal 渲染管线的对象、大图片解码器),这种"代理延迟创建"可以显著提升 App 启动速度——先在启动流程中快速返回 proxy 占位,等用户真正用到该功能时再初始化。

# 7.3 对比NSObject转发

对比项 NSObject 子类做转发 NSProxy 子类做转发
自动转发的方法 只转发"自身没实现"的方法 几乎转发所有方法(包括 NSObject 协议的)
respondsToSelector: 如果自己没实现,返回 NO(不会转到目标) 可以重写让结果反映目标的能力
hash / isEqual: 不在转发范畴内(NSObject 已实现) 会进入转发
内存开销 一个完整的 NSObject(实例变量区 + 很多方法) 极轻量(基本只有一个 isa + 少量 ivar)

结论:如果你需要做"透明代理"——比如把对 A 的所有访问都无缝转给 B——用 NSProxy;如果只是"帮 A 补几个缺失的方法",用 NSObject 子类重写转发方法就够了。

# 8. 性能与调试

# 8.1 三步转发的开销梯度

用 Instruments 实测(iPhone 14, A15, arm64),100 万次方法调用:

调用方式 耗时 相当于直接调用的倍数
直接调用(方法存在) 3.2 ms 1×
resolveInstanceMethod: 动态添加后调用 8.6 ms 2.7×(首次触发解析 + 写入缓存)
forwardingTargetForSelector: 12.4 ms 3.9×(换对象重查)
forwardInvocation: 410 ms 128×(构造 NSInvocation + 装箱参数)

结论:三步转发的开销是指数级上升的——这反过来印证了 2.2 节 "为什么分三步" 的设计哲学:把大部分补救挡在前两道门槛,不让它们付出完整转发的代价。

# 8.2 线上unrecognized selector监控

线上无法附 Xcode 调试,但可以通过 doesNotRecognizeSelector: 的异常来监控"哪里有未实现的方法调用"。然而 ObjC 的异常机制不能简单的用 @try-@catch 拦截——因为 NSObject 内部在抛异常前会走一堆清理逻辑。

更优雅的方案——在 NSObject 的分类里覆盖 forwardingTargetForSelector::

@implementation NSObject (UnrecognizedSelectorGuard)

- (id)swizzled_forwardingTargetForSelector:(SEL)aSelector {
    // 1. 先检查:这个 selector 是不是"看起来该实现、但没实现"的
    NSString *selName = NSStringFromSelector(aSelector);
    
    // 2. 过滤掉系统私有方法(避免误报)
    if ([selName hasPrefix:@"_"] || [selName hasPrefix:@"."]) {
        return [self swizzled_forwardingTargetForSelector:aSelector];
    }
    
    // 3. 上报到监控平台
    [Bugly reportUnrecognizedSelector:selName inClass:NSStringFromClass([self class])];
    
    // 4. 返回一个万能接收者(兜底,防止崩溃)
    return [UnrecognizedSelectorGuard sharedInstance];
    
    // 注意:需要同时重写 methodSignatureForSelector: 和 forwardInvocation:
    // 让万能接收者能处理任意方法调用
}

@end

⚠️ 线上保护的风险:静默吞掉 unrecognized selector 可能掩盖真实 bug——你不再收到崩溃,但业务逻辑在"一个不该走到预期分支"的路径上静默执行。建议只对已知的非关键路径做保护,保留关键路径的崩溃上报。

# 8.3 Method Swizzling本质

Method Swizzling 的原理一句话:交换两个方法的 IMP 指针。

+ (void)load {
    Method original = class_getInstanceMethod([self class], @selector(viewWillAppear:));
    Method swizzled = class_getInstanceMethod([self class], @selector(xxx_viewWillAppear:));
    method_exchangeImplementations(original, swizzled);
}

底层做了什么:

Swizzling 前:
@selector(viewWillAppear:)      → IMP_A
@selector(xxx_viewWillAppear:)  → IMP_B

Swizzling 后:
@selector(viewWillAppear:)      → IMP_B    ← 现在调用 viewWillAppear 实际执行 IMP_B
@selector(xxx_viewWillAppear:)  → IMP_A

这和消息转发的本质联系:Swizzling 在查找阶段就把 IMP 换了——objc_msgSend 按 SEL → IMP 正常查找,结果直接拿到替换后的 IMP,不经过任何转发门槛。所以 Method Swizzling 没有转发开销。

# 9. 实战应用模式

# 9.1 防崩溃(消息保护)

业界常用 forwardingTargetForSelector: 做全局防崩溃——把所有 unrecognized selector 重定向到一个安全桩:

@interface SafeStub : NSObject
@end

@implementation SafeStub

// 万能方法签名——返回任意签名的空壳
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    // 构造一个返回值 void、参数为 (id, SEL, ...任意) 的签名
    return [NSMethodSignature signatureWithObjCTypes:"v@:............"]; 
}

// 什么都不做,吞掉调用
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    // 静默吞掉
}

@end

更安全的做法——在吞掉前先打日志 / 上报:

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSString *stack = [NSThread callStackSymbols].description;
    NSLog(@"⚠️ Unrecognized selector caught: %@, stack: %@",
          NSStringFromSelector(anInvocation.selector), stack);
    // 上报到 Crash 监控 SDK
}

# 9.2 多继承效果

ObjC 不支持多继承,但可以让对象 A "承载" 多个内部对象,通过 forwardingTargetForSelector: 自动路由——本质上是组合模式 + 消息转发:

@interface MultiDelegate : NSObject
@property (nonatomic, strong) NSMutableArray<id> *delegates;
@end

@implementation MultiDelegate

- (BOOL)respondsToSelector:(SEL)aSelector {
    for (id delegate in self.delegates) {
        if ([delegate respondsToSelector:aSelector]) return YES;
    }
    return [super respondsToSelector:aSelector];
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    for (id delegate in self.delegates) {
        if ([delegate respondsToSelector:aSelector]) return delegate;
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end

注意:forwardingTargetForSelector: 只能返回一个对象——多个 delegate 只能顺次匹配第一个;如果需要都收到通知,需要用 forwardInvocation:(把 invocation 依次对每个 delegate 调用 invokeWithTarget:)。

# 9.3 JSPatch热修复原理

JSPatch 的核心原理——用消息转发把 ObjC 方法调用桥接到 JavaScript 引擎:

[obj doSomething:arg]
        │
        ▼
objc_msgSend → 找不到 IMP → resolveInstanceMethod:
        │
        ▼
JSPatch 在此注入——调用 class_addMethod,添加一个统一的 "消息处理函数"
这个函数的 IMP 是 JSPatch 的 JS 执行入口
        │
        ▼
所有未实现的 ObjC 方法调用 → 被包装成 JS 调用 → JavaScriptCore 执行 JS 脚本
        │
        ▼
JS 脚本内部可以调用回 ObjC(通过 JSContext 绑定)

简化版的 JSPatch 核心注入代码:

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    // 1. 给所有没实现的方法统一添加一个"中介 IMP"
    class_addMethod([self class], sel, (IMP)JPForwardInvocation, "v@:@");
    return YES;
}

// 2. 这个中介 IMP 把调用转给 JS 引擎
static void JPForwardInvocation(id slf, SEL sel, ...) {
    // 把 sel + 参数包装成 JS 调用
    // 在 JavaScriptCore 里执行对应的 JS 函数
}

📌 JSPatch 2017 年被 App Store 审核封禁(因为 "动态下发可执行代码" 违反条款)——但不影响我们理解它的技术原理。

# 9.4 Aspects AOP实现

Aspects (opens new window) 是 iOS AOP(面向切面编程)的经典库——可以在方法执行前、后、或替换它:

[UIViewController aspect_hookSelector:@selector(viewWillAppear:)
                           withOptions:AspectPositionBefore
                            usingBlock:^(id<AspectInfo> info) {
    NSLog(@"%@ 将要出现", info.instance);
} error:nil];

底层原理(三部曲):

  1. 把原始 SEL 的 IMP 换成一个统一的 "消息转发入口"(Method Swizzling)
  2. 把原始 IMP 存下来(保存到 aliasSelector 的 IMP 位置)
  3. 在 "消息转发入口" 里,先调用 hook block,再调原始 IMP
原始调用链路:
[vc viewWillAppear:] → IMP_viewWillAppear

Aspects hook 后:
[vc viewWillAppear:]
    → (Swizzled) IMP_aspectsForward
        ├── 1. 调用 before-hook block
        ├── 2. 通过 forwardInvocation 调用原始 IMP_viewWillAppear
        └── 3. 调用 after-hook block

可以看到,Aspects 把 Method Swizzling(换 IMP)+ 消息转发(forwardInvocation 统一入口)结合起来,用消息转发做了一层 "hook 调度器"。

# 10. 综合案例串讲

# 10.1 案例真相揭晓

回到第 1 章的 onBuyButtonTapped: 崩溃,七个疑问现在能逐条作答:

疑问 答案
① objc_msgSend 是怎么找到 IMP 的? 第 3 章:cache 哈希 → 类 method_list → 父类链 → 转发
② 为什么缓存能让查找接近 0 开销? 第 3.2:cache_t 是哈希表,SEL 直接做 key,一次哈希命中
③ isa 链:实例→类→元类到底怎么走? 第 3.3:实例 isa→类,类 isa→元类,元类 isa→根元类
④ 找不到方法时几次补救? 第 4/5/6 章:三次——动态解析 → 快速转发 → 完整转发
⑤ resolve / forwardingTarget / forwardInvocation 区别? 第 4/5/6:添加方法 / 换人执行 / 任意控制(修改参数、返回值)
⑥ NSProxy 和 NSObject 转发有何不同? 第 7 章:NSProxy 几乎全部方法都转发,NSObject 只转发未实现的方法
⑦ 转发性能代价多大? 第 8.1:forwardInvocation 比直接调用慢 ~128 倍

三种修复方案(按工程实践推荐排序):

方案 A:补上实现(治本)

@implementation OrderViewController
- (void)onBuyButtonTapped:(UIButton *)sender {
    // 实现购买逻辑
    [self showPaymentPage];
}
@end

代价:需要重新发版。

方案 B:全局防崩溃保护(治标,热修复)

// 在 NSObject 的 Category 中做全局保护
@implementation NSObject (CrashGuard)

+ (void)load {
    // Swizzle forwardingTargetForSelector: 到自定义版本
    // 让所有 unrecognized selector 不崩溃
}

@end

代价:会掩盖真实 bug,建议集成上报。

方案 C:resolveInstanceMethod 动态注入

+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(onBuyButtonTapped:)) {
        IMP imp = imp_implementationWithBlock(^(id self, UIButton *sender) {
            NSLog(@"onBuyButtonTapped 被动态注入了");
        });
        class_addMethod([self class], sel, imp, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

代价:需要在基类或分类中为每个可能缺失的方法写注入代码;适合已知的特定问题,不适合做通用保护。

# 10.2 一个方法调用的一生

把 [obj onBuyButtonTapped:sender] 这一行的全过程串成一棵知识树:

[obj onBuyButtonTapped:sender]
        │
        ├─ 编译期
        │   └─ clang 将方法调用翻译为:
        │       objc_msgSend(obj, @selector(onBuyButtonTapped:), sender)
        │
        ├─ 运行时 ─── 阶段一:消息发送
        │   ├─ nil 检查:obj == nil? → 是 → 返回 nil,结束
        │   ├─ 取 isa → 找类对象
        │   ├─ 查 cache:key = @selector(onBuyButtonTapped:)
        │   │   ├─ 命中 → 取 IMP → 尾调用跳转 → 执行 ✅
        │   │   └─ 未命中 ↓
        │   ├─ 查 method_list(类自身)
        │   │   ├─ 找到 → 写入 cache + 尾调用 ✅
        │   │   └─ 没找到 ↓
        │   ├─ superclass 链查找(逐级往上)
        │   │   ├─ 找到 → 写入 cache + 尾调用 ✅
        │   │   └─ 全没找到 → 进入 _objc_msgSend_uncached
        │   │
        │   ├─ 运行时 ─── 阶段二:动态方法解析
        │   │   ├─ [obj class] resolveInstanceMethod:@selector(onBuyButtonTapped:)
        │   │   │   ├─ 返回 YES → Runtime 重新查找 → 找到(class_addMethod 已注入)✅
        │   │   │   └─ 返回 NO ↓
        │   │   │
        │   │   ├─ 运行时 ─── 阶段三:快速转发
        │   │   │   ├─ [obj forwardingTargetForSelector:] → 返回 target ≠ nil
        │   │   │   │   └─ 重新 objc_msgSend(target, selector, sender) ✅
        │   │   │   └─ 返回 nil ↓
        │   │   │
        │   │   └─ 运行时 ─── 阶段四:完整转发
        │   │       ├─ [obj methodSignatureForSelector:] → 获取方法签名
        │   │       │   ├─ 返回 nil → doesNotRecognizeSelector: → 💥 CRASH
        │   │       │   └─ 返回签名 ↓
        │   │       └─ [obj forwardInvocation:invocation]
        │   │           ├─ 修改参数 / 改目标 / 多目标 ✅
        │   │           └─ 什么都不做 → 静默吞掉 ✅
        │   │
        │   └─ 最终执行 IMP 函数
        │       ├─ 函数内部 self = obj, _cmd = @selector(onBuyButtonTapped:)
        │       ├─ 栈帧在 objc_msgSend 调用者的栈上(尾调用,无额外帧)
        │       └─ ret 返回 → 调用者继续执行

理解一个方法调用的一生,就是理解 ObjC 全部动态性的根基。

# 10.3 设计哲学回扣

整理本篇的四条跨篇适用的设计哲学:

哲学 1:缓存是动态语言的定海神针——"动"在罕见,"快"在常见

ObjC 被贴着 "动态语言" 的标签,但它不慢——cache_t 让 99.9% 的方法调用命中缓存,和 C 函数调用几乎无差别。动态是能力,不是枷锁。同样的设计在 JavaScript 的 JIT(Just-In-Time)编译、Java 的 inline cache 中都能看到——名字不同,本质一样。

哲学 2:渐进式补救——给程序员三次机会,每次代价递增

resolveInstanceMethod: → forwardingTargetForSelector: → forwardInvocation:,三层补救的代价从 2.7× 到 128× 指数增长。这不是设计的缺陷,而是设计的美学——把简单场景的代价压到最低,把复杂能力留给愿意付出代价的人。这正是 "Don't pay for what you don't use" 的体现。

哲学 3:组合优于继承——消息转发是 ObjC 的 "多继承替代品"

ObjC 没有多继承,但 forwardingTargetForSelector: 让你能把任意多个对象的行为组合进一个对象。这是 组合模式 在语言级的最佳实现——比 Java 的 interface default method、C++ 的虚继承都更直接。

哲学 4:fail fast——unrecognized selector 就应崩溃

默认情况下,调了未实现的方法就抛异常——这和 C++ 的 SIGSEGV、Java 的 NoSuchMethodError 一样,是 fail fast 原则的体现。你的代码有问题,系统就立刻告诉你——信号比静默错误珍贵一千倍。只有在明确需要"柔性容错"的场景(如热修复、AOP),才主动 override 转发链路。

# 10.4 消息转发速查表

阶段 方法 参数 返回值 关键用途 性能
消息发送 objc_msgSend (id, SEL, ...) IMP 直接执行 正常方法调用 1×
动态解析 +resolveInstanceMethod: (SEL) BOOL @dynamic 属性、Core Data 2.7×
快速转发 -forwardingTargetForSelector: (SEL) id(备用接收者) 多继承模拟、消息路由 3.9×
完整转发 -methodSignatureForSelector: (SEL) NSMethodSignature* 为 forwardInvocation 提供签名 —
完整转发 -forwardInvocation: (NSInvocation*) void 任意控制(修改参数、多目标、改 selector) 128×
最终崩溃 -doesNotRecognizeSelector: (SEL) void(抛异常) — —

NSInvocation 参数索引表(必须熟记):

index 含义
0 self(接收者)
1 _cmd(SEL,当前方法选择器)
2+ 方法的第 1 个显式参数、第 2 个…

动态添加方法的类型编码速查:

// "v@:@"
//  v = void 返回值
//  @ = 第一个参数:id (self)
//  : = 第二个参数:SEL (_cmd)
//  @ = 第三个参数:id (第一个显式参数)

// "i@:@@"  → 返回 int,参数 id, SEL, id, id
// "d@:i@"  → 返回 double,参数 id, SEL, int, id

下一篇:你已经知道方法调用在 Runtime 内部的完整旅程,下一章进入 03.RunLoop运行循环揭秘 (opens new window)——把 "为什么 iOS App 不会退出,为什么主线程能同时处理触摸 + 网络回调" 从 CFRunLoopSource 级别彻底剖开。

上次更新: 2026/06/28, 17:55:19
iOS系统架构概览
RunLoop运行循环揭秘

← iOS系统架构概览 RunLoop运行循环揭秘→

最近更新
01
科学方法实践论法
06-28
02
辩证思维矛盾论法
06-28
03
毛选中的调查观念
06-28
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式