【iOS】消息传递和消息转发

文章目录

    • 前言
    • 一、消息传递:objc_msgSend 的“查字典+递归找家长”流程
      • 1. 第一步:查“最近调用记录”(方法缓存)—— 最快即快速查找!
      • 2. 第二步:翻“自己的字典”(类方法列表查找)—— 较慢!
      • 3. 第三步:递归“找家长”(父类方法列表)—— 最慢!
      • 总结:消息传递的“三级跳”
    • 二、消息转发:快递送不到时的“三级补救方案”
      • 阶段 1:动态方法解析(自己加方法)——“我马上补一个!”
      • 一、消息传递的本质:`objc_msgSend`的执行流程
        • 1. 快速查找:方法缓存(Method Cache)
        • 2. 类方法列表查找
        • 3. 缓存更新与结果返回
      • 二、消息转发:当消息无法被处理时
        • 阶段 1:动态方法解析(Dynamic Method Resolution)
      • 阶段 2:快速转发(转交给其他对象)——“我找朋友帮忙!”
      • 阶段 3:完整转发(自定义处理流程)——“我自己写个转单系统!”
      • 关键注意点:
    • 三、底层原理:用汇编看 `objc_msgSend` 的“高效魔法”
    • 四、总结

前言

  Objective-C的消息传递与消息转发是其动态特性的核心,基于运行时(Runtime)系统实现。本文将从底层机制出发,详细解析消息传递的完整流程及消息转发的三个关键阶段,并结合源码(如 objc/runtime)和汇编层面进行深入探讨。

一、消息传递:objc_msgSend 的“查字典+递归找家长”流程

当调用 OC 对象的方法时(如 [obj doSomething]),编译器会将其转换为 C 函数调用:

objc_msgSend(obj, @selector(doSomething));

objc_msgSend是 OC 消息传递的核心函数,其本质是在接收者的类及其父类的方法列表中查找目标方法(SEL)的实现(IMP),并执行该实现。我们下面以OC 中调用方法(如 [dog 叫])为例子,本质是让系统帮我们找到方法的实现代码(IMP)并执行。这个过程由 objc_msgSend 函数完成,它的执行逻辑像“查字典+递归找家长”,分三步:

1. 第一步:查“最近调用记录”(方法缓存)—— 最快即快速查找!

OC 运行时会为每个类维护一个方法缓存(methodCache_t,用于加速方法查找。缓存的结构是一个哈希表,键为 SEL(方法选择子),值为 IMP(方法实现的指针)。

objc_msgSend首先检查接收者类的缓存:

  • 若缓存中存在目标 SEL,直接跳转到对应的 IMP执行(零成本缓存命中)。
  • 若缓存未命中,进入类方法列表查找

例如,每个类(如 Dog 类)都有一个 方法缓存(Method Cache),类似手机的“最近通话记录”:

  • 作用:存“最近调用过的方法名(SEL)”和对应的“实现代码地址(IMP)”,下次调用直接查缓存,无需重复计算。
  • 为什么快:哈希表结构,查找时间复杂度接近 O(1)(常数级)。

例子
你上周让 dog 叫过 3 次,系统就把“叫”这个方法名(SEL)和对应的“汪汪汪”实现(IMP)记在 Dog 类的缓存里。这周再调用 [dog 叫]objc_msgSend 直接查缓存,秒级找到 IMP 并执行。

2. 第二步:翻“自己的字典”(类方法列表查找)—— 较慢!

若缓存未命中,objc_msgSend会从接收者的当前类开始,逐级向上遍历继承链(直到 NSObject或根类),在每个类的方法列表(method_list_t)中查找目标 SEL

每个类的方法列表存储了该类自身定义的方法(不包括父类)。若当前类未找到,继续查找其父类的方法列表,直到根类(如 NSObject)的父类为 nil,此时查找失败。

例子
如果缓存里没找到(比如第一次调用 [dog 叫]),objc_msgSend 会去当前类的“字典”(方法列表)里找。每个类的方法列表存着自己定义的所有方法(类似字典的“正文”)。 Dog 类的字典里有 等方法的定义(SEL 是“叫”,IMP 是“汪汪汪”的代码)。objc_msgSend 遍历这个字典,找到“叫”对应的 IMP,执行。

3. 第三步:递归“找家长”(父类方法列表)—— 最慢!

如果当前类的字典里也没有(比如 Dog 类没写 方法),objc_msgSend 会去父类的字典里继续找(类似“问爸爸有没有这个词的解释”)。一直找到根类(如 NSObject)的父类(nil),若最终找到目标 SELIMP,则将该 SELIMP的映射写入当前类的方法缓存(后续调用直接命中缓存),并跳转到 IMP执行方法逻辑;还没找到,就触发消息转发。

总结:消息传递的“三级跳”

调用 [dog 叫] → objc_msgSend 开始:

1.查 Dog 类的缓存 → 找到?直接执行(最快)。

2.没找到 → 查 Dog 类的方法列表 → 找到?执行(较快)。

3.没找到 → 递归查父类(Animal → NSObject)的方法列表 → 找到?执行(较慢)。

4.全没找到 → 触发消息转发(兜底逻辑)。

二、消息转发:快递送不到时的“三级补救方案”

objc_msgSend遍历完缓存、当前类、父类继承链仍未找到目标 SELIMP,OC 运行时会触发**消息转发(Message Forwarding)**机制,尝试通过一系列回调让开发者有机会“补救”未处理的消息。消息转发分为三个阶段,按顺序执行且不可逆(前一阶段成功则后续阶段不再触发)。

阶段 1:动态方法解析(自己加方法)——“我马上补一个!”

Objective-C(OC)的消息传递与消息转发是其动态特性的核心,基于运行时(Runtime)系统实现。本文将从底层机制出发,详细解析消息传递的完整流程及消息转发的三个关键阶段,并结合源码(如 objc/runtime)和汇编层面进行深入探讨。

一、消息传递的本质:objc_msgSend的执行流程

当调用 OC 对象的方法时(如 [obj doSomething]),编译器会将其转换为 C 函数调用:

objc_msgSend(obj, @selector(doSomething));

objc_msgSend是 OC 消息传递的核心函数,其本质是在接收者的类及其父类的方法列表中查找目标方法(SEL)的实现(IMP),并执行该实现。整个过程可分为以下步骤:

1. 快速查找:方法缓存(Method Cache)

OC 运行时会为每个类维护一个方法缓存(methodCache_t,用于加速方法查找。缓存的结构是一个哈希表,键为 SEL(方法选择子),值为 IMP(方法实现的指针)。

objc_msgSend首先检查接收者类的缓存:

  • 若缓存中存在目标 SEL,直接跳转到对应的 IMP执行(零成本缓存命中)。
  • 若缓存未命中,进入类方法列表查找。
2. 类方法列表查找

若缓存未命中,objc_msgSend会从接收者的当前类开始,逐级向上遍历继承链(直到 NSObject或根类),在每个类的方法列表(method_list_t)中查找目标 SEL

每个类的方法列表存储了该类自身定义的方法(不包括父类)。若当前类未找到,继续查找其父类的方法列表,直到根类(如 NSObject)的父类为 nil,此时查找失败。

3. 缓存更新与结果返回

若最终找到目标 SELIMP,则将该 SELIMP的映射写入当前类的方法缓存(后续调用直接命中缓存),并跳转到 IMP执行方法逻辑。

二、消息转发:当消息无法被处理时

objc_msgSend遍历完继承链仍未找到目标 SELIMP,OC 运行时会触发**消息转发(Message Forwarding)**机制,尝试通过一系列回调让开发者有机会“补救”未处理的消息。消息转发分为三个阶段,按顺序执行且不可逆(前一阶段成功则后续阶段不再触发)。

阶段 1:动态方法解析(Dynamic Method Resolution)

运行时首先调用类的类方法 +resolveInstanceMethod:(针对实例方法)或 +resolveClassMethod:(针对类方法),允许开发者动态添加方法实现。系统先问当前类:“你能自己写一个这个方法吗?”(调用 +resolveInstanceMethod:)。这时候我们可以用 class_addMethod 动态添加方法实现,相当于“临时补字典条目”。

例子
我们发现 Dog 类忘记实现 方法,于是在 +resolveInstanceMethod: 里补上:

+ (BOOL)resolveInstanceMethod:(SEL)sel {if (sel == @selector(叫)) { // 动态添加方法:SEL 是“叫”,IMP 是“汪汪汪”的代码class_addMethod(self, sel, (IMP)叫的实现, "v@:"); return YES; // 返回YES,表示消息已经被处理,即告诉系统:“我自己解决了!”,objc_msgSend会重新尝试发送消息(此时缓存已更新)。}return [super resolveInstanceMethod:sel];
}// 方法实现(IMP)
void 叫的实现(id self, SEL _cmd) {NSLog(@"汪汪汪!");
}

如果成功,系统会把新方法加入缓存,下次调用直接命中。

阶段 2:快速转发(转交给其他对象)——“我找朋友帮忙!”

如果动态解析失败(+resolveInstanceMethod:返回 NO),运行时会调用实例方法 -forwardingTargetForSelector:,允许开发者指定一个备用接收者(Forwarding Target),将消息转发给该对象处理。动态解析失败比如你不想自己加方法,系统问:“你能找个朋友(其他对象)帮我处理吗?”(调用 -forwardingTargetForSelector:)。你返回一个能处理该消息的对象,相当于“把快递转交给邻居”。

例子
Dog 类发现自己不会“叫”,但它的朋友 Cat 类会,于是返回 Cat 的实例:

- (id)forwardingTargetForSelector:(SEL)aSelector {if (aSelector == @selector(叫)) {return [Cat new]; // 找 Cat 帮忙}return [super forwardingTargetForSelector:aSelector];
}

若返回非 nil对象,消息会被发送给该对象(相当于“代理”模式);若返回 nil,进入下一阶段。上述代码中系统会把 [dog 叫] 转发给 Cat 对象,如果 Cat 会“叫”,消息就被正确处理。

阶段 3:完整转发(自定义处理流程)——“我自己写个转单系统!”

若前两阶段均失败(一般是快速转发未提供备用接收者),运行时会触发完整的消息转发流程,核心是构造一个 NSInvocation对象封装消息信息,并调用 -forwardInvocation:方法让开发者自定义处理。就例如你找不到能帮忙的对象,系统启动“完整转发”:把消息(谁发的、方法名、参数)打包成 NSInvocation 对象,调用 -forwardInvocation: 让你自定义处理。你需要自己决定如何处理这个消息(比如转给其他对象、修改参数、记录日志)。

例子
你重写 -forwardInvocation:,把消息转给 Cat,并记录日志:

- (void)forwardInvocation:(NSInvocation *)invocation {// 1. 获取原消息的信息(方法名、参数)SEL sel = invocation.selector;id target = [Cat new]; // 临时目标// 2. 修改消息目标为 Cat[invocation setTarget:target]; // 改成转给 Cat[invocation invoke]; // 重新发送消息// 3. (可选)获取返回值并处理id result;[invocation getReturnValue:&result];NSLog(@"转发成功,结果是:%@", result);
}// 必须实现:获取目标方法的签名(NSMethodSignature),用于描述方法的参数、返回值类型等信息。若未实现此方法,会直接抛出 unrecognized selector异常。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {if (aSelector == @selector(叫)) {return [NSMethodSignature signatureWithObjCTypes:"v@:"]; // 无参数,返回 void}return [super methodSignatureForSelector:aSelector];
}

-methodSignatureForSelector:未实现或返回 nil,运行时会直接抛出 NSInvalidArgumentExceptionunrecognized selector sent to instance)。

关键注意点:

  • 如果 -methodSignatureForSelector: 没实现或返回 nil,系统会直接抛出 unrecognized selector sent to instance 崩溃(常见错误)。

三、底层原理:用汇编看 objc_msgSend 的“高效魔法”

objc_msgSend 是用 ARM64 汇编 写的,核心逻辑用几行伪代码概括:

objc_msgSend:// 1. 检查接收者是否为 nil(OC 允许向 nil 发消息)cbz x0, LReturnNil  // 如果 receiver 是 nil,直接返回 0// 2. 查缓存:从 receiver 的 isa 指针找到类,然后在缓存里找 SELldr x1, [x0]        // x1 = receiver->isa(类的地址)CacheLookup         // 汇编指令:在类的缓存里查 SEL 对应的 IMP// 3. 缓存命中:直接跳转到 IMP 执行br x2               // x2 是缓存的 IMP 地址,跳转执行LReturnNil:mov x0, #0          // 返回 0(对应 nil 消息的处理)ret

为什么快:缓存查找是汇编级别的优化,几乎无额外开销;方法列表查找是递归遍历,但仅在缓存未命中时触发。

四、总结

OC 的消息传递与转发机制,本质是 “运行时动态性” 的体现:

  • 高效性:通过缓存和方法列表的层级查找,平衡了“首次调用”和“重复调用”的性能。
  • 灵活性:消息转发的三阶段设计,允许开发者在运行时动态修复未处理的方法(如 KVO、动态代理)。

一句话总结objc_msgSend 像一个“智能快递员”,先查最近记录(缓存),再翻自己家抽屉(方法列表),最后递归问家长(父类);找不到时,系统给你三次“补救机会”(动态解析→快速转发→完整转发),确保消息“不轻易丢失”。**

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/89632.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/89632.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL查询优化与事务实战指南

本节用到的员工信息管理表结构放到资源中,需要的同学自取。本节内容以此表为示例: 面试题:innodb与myisam的区别。 外键,事务 特性InnoDBMyISAM事务支持支持不支持外键支持不支持锁粒度行级锁表级锁索引结构聚簇索引非聚簇索引崩…

Windows 10/11 磁盘清理操作指南:彻底解决系统盘空间不足问题

🧑 博主简介:CSDN博客专家、CSDN平台优质创作者,高级开发工程师,数学专业,10年以上C/C, C#,Java等多种编程语言开发经验,拥有高级工程师证书;擅长C/C、C#等开发语言,熟悉Java常用开发…

b-up:Enzo_Mi:深度学习基础知识

1.最近邻差值(Neareast Neighbor Interpolation) 插值算法 | 最近邻插值法_哔哩哔哩_bilibili 上图中最后一行,第一个图像,因为目标像素(放大后,位于第1行第0列的像素)距离它最近的…

微信小程序商品结算功能

整体结算流程概述微信小程序的商品结算涉及前端交互、API调用和数据管理。典型流程包括:用户交互:用户选择商品、填写地址和时间。数据获取:从小程序缓存或后端服务器获取订单信息。逻辑处理:验证参数、应用红包折扣。提交订单&am…

2025年7月份最新一区算法——向光生长算法

注:该算法已按照智能优化算法APP标准格式进行整改,可直接集成到APP中,方便大家与自己的算法进行对比。(近期智能优化算法APP将会迎来超级大更新!请时刻保持关注哦!)向光生长算法(Pho…

脚手架新建Vue2/Vue3项目时,项目文件内容的区别

一. package.json vue版本号不同vue2中会多一个依赖:vue-template-compiler,作用是预编译Vue2模板为渲染函数,减少运行时开销。vue-template-compiler与vue版本要保持一致,否则会报错。eslintConfig中的extends不同 eslintConfig…

微信小程序入门实例_____从零开始 开发一个每天记账的微信小程序

在前面的微信小程序实例中我们开发了体重记录等实用小程序,今天来尝试一个和生活消费紧密相关的 ——“每日记账小程序”。它能帮你随时记录收支情况,让每一笔花费都清晰可查。下面就跟着步骤,一步步构建这个小程序。​体验一个开发者的快乐。…

2026python实战——如何利用海外代理ip爬取海外数据

家人们!随着跨境电商的发展,是不是越来越多的小伙伴们也开始搞海外的数据分析了?不过虽然我们已经整天爬虫、数据采集打交道了,但一到海外数据,还是有不少人掉进坑里。你们是不是也遇到过以下情况:花了一堆…

Spring Boot启动原理:从main方法到内嵌Tomcat的全过程

Spring Boot的启动过程是一个精心设计的自动化流程,下面我将详细阐述从main方法开始到内嵌Tomcat启动的全过程。 1. 入口:main方法 一切始于一个简单的main方法: SpringBootApplication public class MyApplication {public static void m…

小白学Python,网络爬虫篇(1)——requests库

目录 一、网络爬虫的介绍 1.网络爬虫库 2.robots.txt 规则 二、requests 库和网页源代码 1.requests 库的安装 2.网页源代码 三、获取网页资源 1.get () 函数 (1)get() 搜索信息 (2)get() 添加信息 2.返回 Response 对象…

平板可以用来办公吗?从文档处理到创意创作的全面测评

在快节奏的现代职场,一个核心疑问始终萦绕在追求效率的职场人心中:平板电脑,这个轻薄便携的设备,真的能替代笔记本电脑,成为值得信赖的办公伙伴吗? 答案并非简单的“是”或“否”,而是一个充满潜…

docker gitlab 备份 恢复 版本升级(16.1.1到18.2.0)

docker 启动 # 在线 docker pull gitlab/gitlab-ce:latest # 离线 docker save -o gitlab-ce-latest.tar gitlab/gitlab-ce:latest docker load -i gitlab-ce-latest.tardocker run --detach \--publish 8021:80 --publish 8023:22 \ --name gitlab_test \--restart always \-…

web3 区块链技术与用

#53 敲点算法题 瑞吉外卖day4 调整心态 睡眠 及精神 web3 以下是应北京大学肖臻老师《区块链技术与用》公开课的完整教学大纲,综合课程内容、技术模块及前沿扩展,分为核心章节与专题拓展两部分,引用自公开课资料及学员笔记。 &#x1f4…

Redis1:高并发与微服务中的键值存储利器

redis中存储的数据格式为键值对(Key,Value)在高并发的项目和微服务的项目会频繁的用到redisNoSQL型数据库1.初始Redis1.1认识NoSQLSQL:structure query language关系型数据库结构化:有固定格式要求(表关系,…

/字符串/

字符串 个人模板 5. 最长回文子串 93. 复原 IP 地址 43. 字符串相乘 227. 基本计算器 II

我的开发日志:随机数小程序

文章目录前言UI设计代码前言 为什么我要设计这个程序呢?因为我要用,懒得在网上下载了,于是干脆写了一个。 UI设计 UI是我凹出来的,你们要使用,直接新建一个UI.ui文件,然后把下面的东西输进去就可以了。 …

《Oracle SQL:使用 RTRIM 和 TO_CHAR 函数格式化数字并移除多余小数点》

select RTRIM(to_char(1222.11123344,fm9999990.9999),.) from dual 这条 SQL 语句主要用于对数字进行格式化处理,并移除格式化结果右侧多余的小数点。下面将详细拆解该语句的执行过程和各部分作用。语句详细拆解1. to_char(1222.11123344,fm9999990.9999)函数功能&…

「Java案例」方法重装求不同类型数的立方

利用方法重装实现不同类型数值的立方计算 立方计算方法的重载实现 编写一个程序,要求编写重载方法xxx cube(xxx value)实现对不同类型数值计算立方。 # 源文件保存为“CubeCalculator.java” public class CubeCalculator {public static void main(String[] args) {// 测试…

API 接口开发与接入实践:自动化采集淘宝商品数据

在电商数据分析、价格监控等场景中,自动化采集淘宝商品数据具有重要价值。本文将详细介绍如何通过 API 接口开发实现淘宝商品数据的自动化采集,包含完整的技术方案和代码实现。 一、淘宝 API 接入基础 1. 接入流程概述 注册淘宝账号获取 ApiKey 和 Ap…

python-pptx 的layout 布局

一、布局基础概念 在 PowerPoint 中,布局(Layout) 决定了幻灯片的占位符(如标题、内容、图片等)的排列方式。python-pptx 提供了对布局的编程控制。二、默认布局类型及索引 通过 prs.slide_layouts[index] 访问&#x…