RunTime 消息机制简介
在 Objective-C 中,方法的调用都是类似 [receiver selector];
的形式,其本质是让对象在运行时动态地发送消息。我们来看看 receiver
对象调用 selector
方法时,在『编译阶段』和『运行阶段』分别会发生什么:
-
编译阶段
[receiver selector];
方法调用被编译器转换为:objc_msgSend(receiver, selector);
(不带参数)objc_msgSend(recevier, selector, org1, org2, …);
(带参数)
-
运行时:消息发送阶段
消息接受者
recevier
寻找对应的selector
:- 通过
recevier
的isa
指针找到recevier
对应的Class
(类) - 在
Class
(类)的Cache
(方法缓存)的散列表中寻找对应的IMP
(方法实现) - 如果在
Cache
(方法缓存)中没有找到对应的IMP
(方法实现),则继续在Class
(类)的Method List
(方法列表)中找寻找对应的selector
。如果找到,则填充到Cache
(方法缓存)中,并返回selector
- 如果在
Class
(类)中没有找到这个selector
,则继续在recevier
的superclass
(父类)中寻找 - 一旦找到对应的
selector
,则直接执行selector
对应的IMP
(方法实现) - 如果找不到对应的
selector
,则 RunTime 系统进入消息转发阶段
- 通过
-
运行时:消息转发阶段
① 动态方法解析:通过重写当前recevier
的+resolveInstanceMethod:
(对象方法)或者+resolveClassMethod:
(类方法),利用 RunTime 的class_addMethod
函数给当前recevier
动态地添加其他方法实现② 消息接受者重定向:如果上一步中没有添加其他方法实现,则可重写当前
recevier
的-forwardingTargetForSelector:
(对象方法)或者+forwardingTargetForSelector:
(类方法),将消息动态地转发给其他对象处理③ 完整的消息重定向:如果上一步的返回值为
nil
或者self
,则可重写当前recevier
的-methodSignatureForSelector:
(对象方法)或者+methodSignatureForSelector:
(类方法),获取消息的参数和返回值类型- 如果
methodSignatureForSelector:
返回了一个NSMethodSignature
对象(方法签名),则 RunTime 系统就会创建一个NSInvocation
对象,并调用当前recevier
的-forwardInvocation:
(对象方法)或者+forwardInvocation:
(类方法),给予此次消息发送最后一次寻找IMP
(方法实现)的机会 - 如果
recevier
的methodSignatureForSelector:
返回 nil,则 RunTime 系统发出-doesNotRecognizeSelector:
(对象方法)或者+doesNotRecognizeSelector:
(类方法)消息,程序也就崩溃了
- 如果
-
RunTime 消息机制的核心目的
通过以上对 RunTime 消息机制的简单介绍,我们对 RunTime 消息机制有了总体上的印象
接下来我们将以 RunTime 消息机制的入口函数
objc_msgSend
为起点,对 RunTime 消息机制的整个过程进行全面且详细的讲解。其中涉及到 arm 汇编和繁琐的函数调用,需要读者对 arm 汇编以及 RunTime 的基本数据结构有一定的了解因为 RunTime 消息机制中涉及到很多边界情况和意外情况的处理,所以初读本文可能会迷失在茫茫的细节中。在这里,我为读者竖起一座灯塔:RunTime 消息机制的核心目的是根据方法名称(sel)寻找对应的方法实现(imp),并跳转到该方法实现(imp)处去执行。 接下来介绍的所有汇编代码、函数调用、数据结构,都是围绕此核心目的展开,并为此核心目的服务。读者只要牢记此核心目的来阅读本文,就能加快对本文的理解
消息发送阶段
消息发送阶段的代码可以分为 2 部分:
-
快速查找方法实现。这一部分的代码用汇编实现,负责在消息接受者所属的类对象的方法缓存中查找方法实现
-
慢速查找方法实现。这一部分的代码用 C 实现,负责在消息接受者所属的类对象的方法列表,以及父类对象的方法缓存和方法列表中,查找方法实现
-
消息发送阶段:快速查找方法实现
objc_msgSend
为 RunTime 消息机制的入口函数,其声明为:// path: objc4-756.2/runtime/message.h /* 基本的消息发送原语 在某些 cpu 架构中,如果消息的返回值类型为 struct,则需要使用 objc_msgSend_stret 在某些 cpu 架构中,如果消息的返回值类型为 float,则需要使用 objc_msgSend_fpret 在某些 cpu 架构中,如果消息的返回值类型为 float,则需要使用 objc_msgSend_fp2ret 当使用此函数进行消息发送时,需要将此函数强制转换成适当的函数指针类型 */ void objc_msgSend(void /* id self, SEL op, ... */ );
objc_msgSend
函数主要负责的有:- 获取消息接受者所属的类对象
- 获取类对象的方法缓存
- 通过方法名称(selector)在方法缓存中寻找方法实现(imp)
- 如果命中方法缓存,则跳转到对应的方法实现(imp)处去执行
- 如果没有命中方法缓存,则进入下一个部分(慢速查找方法实现)
我们将在 arm64 架构下分析
objc_msgSend
函数的汇编代码,在开始分析之前我们需要了解:- 在 arm64 架构的 cpu 中有 31 个 64 bit 的整数寄存器,分别被标记为 x0 - x30
- 每一个整数寄存器可以分离开来只使用低 32 bit,分别被标记为 w0 - w30
- 其中 x0 - x7 用于传递函数的参数,这意味着
objc_msgSend
函数接收的self
参数被放在 x0 中,接收的_cmd
参数被放在 x1 中 - p 寄存器为指针寄存器(p registers are pointer-sized),是在 arm64-asm.h 中为 x 寄存器或者 w 寄存器通过宏定义取的别名。在 arm64 架构的 cpu 中,一个指针变量占 8 个 Byte,此时 p 寄存器等价于 x 寄存器。在 arm64_32 架构的 cpu 中,一个指针变量占 4 个 Byte,此时 p 寄存器等价于 w 寄存器
objc_msgSend
函数的汇编实现为:// path: objc4-756.2/runtime/objc-msg-arm64.s // objc_msgSend 函数入口点 ENTRY _objc_msgSend UNWIND _objc_msgSend, NoFrame // 这条指令用于比较 self 和 0 的大小。b 是跳转,le 是小于等于。如果 self <= 0,则跳转到 LNilOrTagged 进行处理 // self == 0,代表 self 为 nil。self < 0,代表 self 是 Tagged Pointer。LNilOrTagged 用于处理 self 为 nil 或者 self 为 Tagged Pointer 的情况 // 在 arm64 架构中 Tagged Pointer 会设置 self 的高位来标识自己。当 self 的高位被设置时,self 会被解析成一个负数 // p0 = self 即 x0 = self cmp p0, #0 b.le LNilOrTagged // 将 x0 指向的数据读取到 x13。也就是将 self 所指向的对象赋值给 x13 // 因为 x13 能存储 8 Byte,又因为一个实例对象的前 8 Byte 为 isa 指针,所以 x13 实际上存储的是 self->isa 指针 // p13 = isa 即 x13 = isa ldr p13, [x0] // arm64 架构可以使用 non-pointer isa。一般来说 isa 指针指向的是一个实例对象所属的类对象,但是 non-pointer isa 会在闲置的 bit 上插入一些信息 // 这一步主要是移除 isa 指针上存储的多余信息,将一个真实指向类对象的指针保存在 x16 里 // p16 = class 即 x16 = class GetClassFromIsa_p16 p13 LGetIsaDone: // 查找方法缓存(使用 NORMAL 模式)(calls imp or objc_msgSend_uncached) CacheLookup NORMAL // 处理 self 为 nil 或者 self 为 Tagged Pointer 的情况 LNilOrTagged: // 处理 self 为 nil 的情况 b.eq LReturnZero // 处理 self 为 Tagged Pointer 的情况 // Tagged Pointer 工作原理: // Tagged Pointer 用于支持多种类 // 在 arm64 架构下,当 self < 0 时,self 的高 4 位用来标识 self 所属的类。这 4 个 bit 算是 self 本质上的 isa // 当然,仅仅依靠这 4 个 bit 是不足以表示一个指向类对象的指针的。为此 RunTime 内部会有一个特殊的类表用来存储可用于 Tagged Pointer 的类对象,并且这个类表是私有的 // 所有 Tagged Pointer 类型的 self 所属的类对象都是根据 self 指针的高 4 位从这个私有类表中查询获得的 // 除此之外,Tagged Pointer 还可以拓展其所支持的类。当 self 的高 4 位都被置为 1 的时候,接下来的 8 个 bit 会被用来作为(拓展的 Tagged Pointer 类表)的索引 // 这样做可以在运行时使用较少的存储空间来支持更多的 Tagged Pointer 类 // tagged // 这两条指令用于将私有的 Tagged Pointer 类表 _objc_debug_taggedpointer_classes 的地址加载进 x10 中 // arm64 架构要求加载一个符号的地址时必须使用两条指令,这是一个类精简指令集的架构中独立的技术 // 因为在 arm64 架构下指针的大小是 64 bit 然而指令却只能操作 32 bit 的符号地址,所以只靠一条指令来加载一个完整的符号地址是不可能做到的 // x86 架构就没有这样的麻烦,这是因为 x86 架构有变长的指令。只需要一条 10 Byte 的指令,其中 2 个 Byte 用于标识指令本身和目标寄存器,剩余 8 Byte 用于标识符号的地址 // 在一台使用固定长度指令的机器上,只能一块一块地加载数据。在这个例子中,需要 2 块数据,adrp 指令先加载首部的数据到 x10 中,然后通过 add 指令将剩余部分附加到 x10 中 adrp x10, _objc_debug_taggedpointer_classes@PAGE add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF // 这条指令用于从 x0 寄存器的第 60 位开始,读取 4 位到 x11 寄存器,然后 x11 寄存器剩余的高位用 0 填充 // 此时 x11 寄存器中保存的是 Tagged Pointer 类在私有类表 _objc_debug_taggedpointer_classes 中的索引 ubfx x11, x0, #60, #4 // 这条指令使用保存在 x11 中的索引,从 x10 指向的私有类表 _objc_debug_taggedpointer_classes 中获取 Tagged Pointer 类,然后保存进 x16 ldr x16, [x10, x11, LSL #3] // 这两条指令用于将未识别的 Tagged Pointer 类表 _OBJC_CLASS_$___NSUnrecognizedTaggedPointer 的地址加载进 x10 中 adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF // 这条指令用于检查 x16 中保存的 Tagged Pointer 类是否未识别 cmp x10, x16 // 这条指令用于跳转回标号 LGetIsaDone 处 // 现在 x16 中保存的是 Tagged Pointer 类,因为接下来 Tagged Pointer 类的处理和普通类的处理是一样的,所以只需要从分支跳回主干即可 b.ne LGetIsaDone // ext tagged // 这两条指令用于将私有的 Tagged Pointer 扩展类表 _objc_debug_taggedpointer_ext_classes 的地址加载进 x10 中 adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF // 这条指令用于从 x0 寄存器的第 52 位开始,读取 8 位到 x11 寄存器,然后 x11 寄存器剩余的高位用 0 填充 // 此时 x11 寄存器中保存的是 Tagged Pointer 扩展类在私有类表 _objc_debug_taggedpointer_ext_classes 中的索引 ubfx x11, x0, #52, #8 // 这条指令使用保存在 x11 中的索引,从 x10 指向的私有扩展类表 _objc_debug_taggedpointer_ext_classes 中获取 Tagged Pointer 扩展类,然后保存进 x16 ldr x16, [x10, x11, LSL #3] // 这条指令用于跳转回标号 LGetIsaDone 处 // 现在 x16 中保存的是 Tagged Pointer 扩展类,因为接下来 Tagged Pointer 扩展类的处理和普通类的处理是一样的,所以只需要从分支跳回主干即可 b LGetIsaDone // 处理 self 为 nil 的情况 LReturnZero: // self 为 nil 的情况的处理和其他的代码完全不同。这一部分没有任何类的查询或者方法的执行,它所有的操作就是返回一个 0 给调用者 // 因为 objc_msgSend 函数其实并不知道调用者期待什么类型的返回值(是一个整型数据、浮点型数据、或者什么都没有),所以这个任务实际上会有一点棘手 // 幸运的是:所有用来保存返回值的寄存器即使它们没有在 objc_msgSend 函数的调用过程中被使用到,它们仍然可以被安全地覆写 // 整型返回值保存在 x0 和 x1 中,浮点型返回值保存在 v0 - v3 中 // 因为 d0 - d3 是 v0 - v3 的后半部分,向 d0 - d3 存值的时候会将 v0 -v3 的前半部分自动置 0,所以下面的 4 条 movi 指令执行完成之后,相关的 v 寄存器都会被置 0 // 你可能会好奇为什么没有将 x0 置 0。答案非常简单,因为 x0 保存的是 self,在当前情况下 self 是 nil,所以 x0 已经是 0 了,无需再多一条指令来清理 x0 // 如果返回值类型是 struct(并且 struct 太大了无法存储到寄存器)时,该如何处理? // 这种情况下需要调用者进行一些配合:首先调用者需要为返回值分配足够大的内存空间,其次 struct 的地址会被保存到 x8,然后调用者根据这个地址将返回值写入到预先分配的内存中 // 因为 objc_msgSend 函数无法获取返回的结构体的大小,所以 objc_msgSend 函数无法清理为结构体所分配的内存 // 为了解决这个问题,编译器会在 objc_msgSend 函数被调用之前生成相应的代码来清理为结构体所分配的内存 // 在执行完所有的置 0 操作之后,程序的控制权会返还给调用者 // x0 is already zero mov x1, #0 movi d0, #0 movi d1, #0 movi d2, #0 movi d3, #0 ret // objc_msgSend 函数结束点 END_ENTRY _objc_msgSend
在
objc_msgSend
函数中调用了宏CacheLookup
进行方法缓存的查找:// path: objc4-756.2/runtime/objc-msg-arm64.s // 查找方法缓存(使用 NORMAL 模式)(calls imp or objc_msgSend_uncached) .macro CacheLookup // 这条指令用于将类对象中的方法缓存读取到 x10 和 x11,更具体地:将 x16 所指向的内存位置偏移 #CACHE 字节处的数据,读取到 x10 和 x11 // 类对象中方法缓存的数据结构如下所示: // typedef uint32_t mask_t; // struct cache_t { // struct bucket_t* _buckets; // 方法缓存哈希表的首地址 // mask_t _mask; // 方法缓存哈希表中能容纳的 bucket 的总数量 - 1 // mask_t _occupied; // 方法缓存哈希表中已经存储的 bucket 的数量 // } // 这条指令执行完成之后,_buckets 被读取到 x10 中,_mask 被读取到 x11 的低 32 位,_occupied 被读取到 x11 的高 32 位 // _occupied 只是简单地描述了方法缓存哈希表中已经存储的 bucket 的数量,实际在 objc_msgSend 函数当中不起任何作用 // _mask 的作用则非常的重要,它是一个描述了方法缓存哈希表的总大小并用于做 & 运算的掩码 // _mask 的值总是 2 的 n 次幂 - 1,以二进制来表示的话,就是一个类似于 0000000011111111 这样一个所有的 1 都在末尾连续排列的数 // 通过 _mask 可以计算出需要查询的 selector 的索引,并且在需要搜索方法缓存哈希表的时候环回到方法缓存哈希表的末尾处 // p1 = SEL, p16 = isa, p10 = buckets, p11 = occupied|mask 即 x1 = SEL, x16 = isa, x10 = buckets, x11 = occupied|mask ldp p10, p11, [x16, #CACHE] // 这条指令用于计算出 x1 中保存的 _cmd 在方法缓存哈希表中的索引 // 因为 x1 保存的是 _cmd,所以 w1 保存的是 _cmd 的低 32 位。w11 如上所述保存的是 _mask // 将 w1 和 w11 进行 & 运算,并将结果保存到 w12。这个结果与(_cmd % (_mask + 1))是等价的 // 从这里可以看出:哈希表的构造方法是除留余数法。将 _cmd 地址的低 32 位所表示的数 除以 方法缓存哈希表总大小,取余即为索引 // w12 = _cmd & mask 即 w12 = index and w12, w1, w11 // 这条指令用于将(_cmd 在方法缓存哈希表中的索引)与(方法缓存哈希表的起始地址)相加,求出目标 bucket 的实际地址并保存到 x12 中 // 注意:在这里,索引 (_cmd & mask) 先右移了 (1+PTRSHIFT) 位,通常情况下 (1+PTRSHIFT) == 4,相当于索引乘以 16。这么做的原因是,方法缓存哈希表中每一个 bucket 的大小为 16 字节 // p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) 即 x12 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) add p12, p10, p12, LSL #(1+PTRSHIFT) // 这条指令用于将 x12 中保存的目标 bucket 指针指向的数据读取到 x17 和 x9 // 因为每一个 bucket 都包含一个 imp 和一个 selector,所以 x17 中保存的是 imp,x9 中保存的是 selector // {imp, sel} = *bucket // p17 = imp, p9 = sel, x12 = bucket 即 x17 = imp, x9 = sel, x12 = bucket ldp p17, p9, [x12] // 这条指令用于将(保存在 x9 中目标 bucket 的 selector)与(保存在 x1 中的 _cmd)进行比较 // 如果两者不一致则说明本次的查询没有成功,跳转到标号 2 处执行 CheckMiss 的相关指令(也就是跳转到处理缓存未命中的情况) // if (bucket->sel != _cmd) // 如果两者一致则说明命中缓存,执行 CacheHit 处的相关指令(即调用目标 bucket 的 imp) 1: cmp p9, p1 b.ne 2f CacheHit $0 // 处理命中方法缓存的情况:会直接调用目标 bucket 的 imp 2: CheckMiss $0 // 处理未命中方法缓存的情况:如果目标 bucket 的 sel 为空,则会调用 __objc_msgSend_uncached 函数进入慢速查找流程 // 如果流程执行到这里,则说明通过索引获取到的目标 bucket 不为空且 selector 又不匹配,这可能是由哈希碰撞引起的 // 前面有提到方法缓存哈希表的构造方法是除留余数法,那么得到的哈希地址是有可能发生冲突的 // 这里方法缓存哈希表处理地址冲突的方式应该是开放定址,即在发生地址冲突的时候寻找下一个空的哈希地址去存储数据 // 这条指令用于将 x12 中保存的目标 bucket 的地址与 x10 中保存的方法缓存哈希表的首地址进行比较 // 如果它们匹配的话,则指令会跳转到标号 3 处,查询的索引会环回到方法缓存哈希表的结尾处倒序查询。一般情况下查询哈希表会正序查询,倒序查询哈希表的做法很少见 // 方法缓存哈希表的查询会从当前位置开始倒序执行,查询过程会依次减小索引直到方法缓存哈希表的首部,之后索引将返回方法缓存哈希表的末尾 // 我不确定为什么这里要采取和一般的从哈希表头部开始逐个递增索引不一样的查询方式,但可以肯定的是这样做的原因是:对于方法缓存而言这种查询方式更加高效 // 如果它们不匹配的话,则程序会继续执行下一条指令 cmp p12, p10 b.eq 3f // 这条指令用于从 x12 中保存的目标 bucket 的地址偏移 16 个字节处读取数据到 x17 和 x9。即读取一个新的 bucket 的数据到 x17 和 x9 // 因为每一个 bucket 都包含一个 imp 和一个 selector,所以现在 x17 中保存的是新 bucket 的 imp,x9 中保存的是新 bucket 的 selector // 注意: // 1.地址后面跟着一个感叹号,它指示 x12 是 write-back 的。即 x12 会先更新自己的值,之后再进行其他的操作 // 下面的这条指令会先执行 x12 -= 16,之后再将 x12 指向的新的 bucket 读取到 x9 和 x17 // 2.因为是倒序遍历,所以实际上读取的是方法缓存哈希表中的上一个 bucket // {imp, sel} = *--bucket ldp p17, p9, [x12, #-BUCKET_SIZE]! // 现在新的 bucket 已经被读取到了 x9 和 x17。程序需要复用之前的代码来判断当前的 bucket 是否匹配。这个环回会返回到之前标号 1 处的指令,用新的值重新执行一遍判断流程 // 如果依旧没有找到匹配的 bucket,则代码会按照这个循环一直执行下去直到匹配成功:或者匹配一个空的bucket,又或者一直找到了方法缓存哈希表的首部 b 1b // 这条指令用于辅助环回查询 // x12 中现在保存的是当前 bucket 的指针,在此处也就是方法缓存哈希表中第一个 bucket 的指针 // w11 中保存的是描述方法缓存哈希表总大小的掩码 // 将 w11 左移 (1+PTRSHIFT) 位扩大 16 倍(w11 中保存的是方法缓存哈希表能容纳的条目的总个数,每一个条目的大小是 16 个字节),然后和 x12 累加并将结果保存到 x12 中 // 指令执行完成之后 x12 中保存的就是指向方法缓存哈希表末尾 bucket 的指针了,之后从这里开始倒序查询 // wrap: p12 = first bucket, w11 = mask // p12 = buckets + (mask << 1+PTRSHIFT) 3: add p12, p12, w11, UXTW #(1+PTRSHIFT) // 下面这一步几乎不太可能遇到 // 虽然方法缓存哈希表有足够的空间,但是会随着新条目的添加而变的臃肿。一个过于庞大的方法缓存哈希表会因为查询过程中经常发生哈希碰撞而变得非常的低效 // 这么做的原因源码中有一个解释: // Clone scanning loop to miss instead of hang when cache is corrupt.(在缓存出现错误的时候,克隆扫描的循环而不是挂起) // The slow path may detect any corruption and halt later.(慢速查询会检测到错误并在稍候停止程序) // 很明显 apple 的工程师明白内存的错误会导致方法缓存哈希表被一些错误的条目填满,跳转到 C 部分的慢速查找代码以结束程序 // 从这里开始,除了标号 3 处的指令与前面的查询流程不同之外,所有指令均与前面的查询流程相同 ldp p17, p9, [x12] 1: cmp p9, p1 b.ne 2f CacheHit $0 2: CheckMiss $0 // 这条指令用于检查方法缓存的查询是否又环回到了方法缓存哈希表的首部 // 如果是,则表明整个方法缓存哈希表已经被遍历查寻了一遍并且没有匹配成功 // 这个时候程序会跳转到标号 3 处的指令 JumpMiss 来执行完整的查寻流程 cmp p12, p10 b.eq 3f ldp p17, p9, [x12, #-BUCKET_SIZE]! b 1b 3: JumpMiss $0 .endmacro
通过宏
CacheLookup
的代码可以看出:- 开始查寻方法缓存时,都会从第一次获取到的索引位置开始倒序遍历方法缓存哈希表,到达表头之后返回表尾继续倒序遍历,直到(匹配成功)或者(查找的 bucket 为空)
- 当遍历方法缓存哈希表的流程返回到第一次获取到的索引的位置时,实际上整个方法缓存哈希表已经被遍历一遍了,并且表中的 bucket 都不为空 且 都不和 objc_msgSend 函数需要搜寻的目标 selector 匹配
- 当查寻再次环回到方法缓存哈希表的首部时,程序会跳转到
JumpMiss
处。当然,通常这种情况是不可能发生的,因为方法缓存哈希表足够大。所以只有当方法缓存哈希表被错误填满时,才有可能出现JumpMiss
的情况。这个情况下我们需要跳转到 C 部分的代码来处理该问题
在宏
CacheLookup
中,调用了宏(CacheHit
、CheckMiss
、JumpMiss
)用于处理方法缓存不同的命中情况:// path: objc4-756.2/runtime/objc-msg-arm64.s // 处理命中方法缓存的情况:根据当前所处模式的不同,选择(调用 imp)或者(返回 imp) // 当 objc_msgSend 函数命中方法缓存时: // x17 中保存的是从方法缓存哈希表中查询到的目标 bucket 的 imp // x12 中保存的是从方法缓存哈希表中查询到的目标 bucket 的地址 // x1 中保存的是 objc_msgSend 的参数 _cmd // CacheHit: x17 = cached IMP, x12 = address of cached IMP, x1 = SEL .macro CacheHit .if $0 == NORMAL // 因为 objc_msgSend 函数使用的是 NORMAL 模式,所以会选择直接调用 imp // 程序从这里开始真正执行目标方法的实现。objc_msgSend 函数的快速查询到这一步结束 // 在当前的硬件支持下,如果所有的参数寄存器都没有被修改并且目标方法完整地接收了所有的参数,则整个查寻过程耗时不超过 3 毫微秒。就像是直接调用了方法一样 TailCallCachedImp x17, x12, x1 .elseif $0 == GETIMP mov p0, p17 cbz p0, 9f AuthAndResignAsIMP x0, x12, x1 9: ret .elseif $0 == LOOKUP AuthAndResignAsIMP x17, x12, x1 ret .else .abort oops .endif .endmacro // 处理未命中方法缓存的情况:根据当前所处模式的不同,选择调用不同的查询函数 .macro CheckMiss // miss if bucket->sel == 0 .if $0 == GETIMP cbz p9, LGetImpMiss .elseif $0 == NORMAL // 因为 objc_msgSend 函数使用的是 NORMAL 模式,所以会执行下面这条指令 // 这条指令用于检查(x9 中保存的目标 bucket 的 selector)是否为 0 // 如果(x9 中保存的目标 bucket 的 selector)== 0,说明之前通过索引取出的目标 bucket 为空,这意味着之前的查询失败了,目标方法的实现并不在方法缓存哈希表当中 // 这时候需要调用 C 函数 __objc_msgSend_uncached 去执行完整的查询流程,即执行慢速查找 cbz p9, __objc_msgSend_uncached .elseif $0 == LOOKUP cbz p9, __objc_msgLookup_uncached .else .abort oops .endif .endmacro // 处理整个方法缓存哈希表已经被遍历查寻了一遍并且没有匹配成功的情况:略过所有条件判断,直接开启慢速查找 .macro JumpMiss .if $0 == GETIMP b LGetImpMiss .elseif $0 == NORMAL // 因为 objc_msgSend 函数使用的是 NORMAL 模式,所以会执行下面这条指令 // 直接调用 C 函数 __objc_msgSend_uncached 去执行完整的查询流程,即执行慢速查找 b __objc_msgSend_uncached .elseif $0 == LOOKUP b __objc_msgLookup_uncached .else .abort oops .endif .endmacro
如果在消息接受者所属的类对象的方法缓存中查找到方法实现(imp),则跳转到对应的方法实现(imp)处去执行,消息发送流程到此结束
如果在消息接受者所属的类对象的方法缓存中没有查找到方法实现(imp),则跳转到
__objc_msgSend_uncached
函数,调用下一个部分(慢速查找方法实现)的流程以获取方法实现(imp),并跳转到对应的方法实现(imp)处去执行:// path: objc4-756.2/runtime/objc-msg-arm64.s // 处理未命中当前类对象的方法缓存的情况 STATIC_ENTRY __objc_msgSend_uncached UNWIND __objc_msgSend_uncached, FrameWithNoSaves // THIS IS NOT A CALLABLE C FUNCTION(这不是一个可调用的 C 函数) // Out-of-band p16 is the class to search(要搜索的类对象的地址保存在 x16 中) // 搜索类对象的方法列表 MethodTableLookup // 查找到方法实现(imp)之后,会将方法实现(imp)保存在 x17 中 // 这条指令用于跳转到 x17 所指向的位置,相当于 br x17。即用于执行方法实现(imp) TailCallFunctionPointer x17 END_ENTRY __objc_msgSend_uncached
在
__objc_msgSend_uncached
函数中,调用了宏MethodTableLookup
宏
MethodTableLookup
用于:- 为执行下一个部分(慢速查找方法实现)的流程做准备
- 跳转到 C++ 函数
_class_lookupMethodAndLoadCache3
开启下一部分(慢速查找方法实现)的流程
// path: objc4-756.2/runtime/objc-msg-arm64.s // 在类对象的方法列表中查找方法实现(imp) .macro MethodTableLookup // 栈平衡:提升栈空间 SignLR stp fp, lr, [sp, #-16]! mov fp, sp // 保存参数寄存器:x0 - x8,q0 - q7 sub sp, sp, #(10*8 + 8*16) stp q0, q1, [sp, #(0*16)] stp q2, q3, [sp, #(2*16)] stp q4, q5, [sp, #(4*16)] stp q6, q7, [sp, #(6*16)] stp x0, x1, [sp, #(8*16+0*8)] stp x2, x3, [sp, #(8*16+2*8)] stp x4, x5, [sp, #(8*16+4*8)] stp x6, x7, [sp, #(8*16+6*8)] str x8, [sp, #(8*16+8*8)] // x0 中保存着 self,x1 中保存着 _cmd,x16 中保存着要搜索方法实现的类对象 // 因为 _class_lookupMethodAndLoadCache3 函数会从 x2 中读取要搜索方法实现的类对象 // 所以将保存在 x16 中的要搜索方法实现的类对象读取到 x2 中 // __class_lookupMethodAndLoadCache3 是汇编符号,对应着 C++ 函数 _class_lookupMethodAndLoadCache3 mov x2, x16 bl __class_lookupMethodAndLoadCache3 // _class_lookupMethodAndLoadCache3 函数查找到方法实现(imp)之后,会将方法实现(imp)保存在 x0 中 // 这条指令用于将保存在 x0 中的方法实现(imp)读取到 x17 中 mov x17, x0 // 恢复参数寄存器:x0 - x8,q0 - q7 ldp q0, q1, [sp, #(0*16)] ldp q2, q3, [sp, #(2*16)] ldp q4, q5, [sp, #(4*16)] ldp q6, q7, [sp, #(6*16)] ldp x0, x1, [sp, #(8*16+0*8)] ldp x2, x3, [sp, #(8*16+2*8)] ldp x4, x5, [sp, #(8*16+4*8)] ldp x6, x7, [sp, #(8*16+6*8)] ldr x8, [sp, #(8*16+8*8)] // 栈平衡:释放栈空间 mov sp, fp ldp fp, lr, [sp], #16 AuthenticateLR .endmacro
-
消息发送阶段:慢速查找方法实现
_class_lookupMethodAndLoadCache3
为慢速查找方法实现的入口函数:// path: objc4-756.2/runtime/objc-runtime-new.mm // 本函数仅针对 objc_msgSend 消息发送过程中的方法查找,其他代码中如果要进行方法查找请使用 lookUpImp 函数 // 本函数不会对当前类 cls 的方法缓存进行扫描,因为 objc_msgSend 在之前已经扫描过当前类 cls 的方法缓存 IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls) { return lookUpImpOrForward(cls, sel, obj, YES/*initialize*/, NO/*cache*/, YES/*resolver*/); }
在
_class_lookupMethodAndLoadCache3
函数中,设置了一些bool
类型的标志位之后,调用了lookUpImpOrForward
函数lookUpImpOrForward
函数主要负责:- 在当前类对象中进行不加锁的方法缓存查找
- 加锁 && 检查当前类对象是否已知
- 如果当前类对象没有实现(
isRealized
)或者初始化(isInitialized
),则实现或者初始化当前类对象 - 尝试在当前类对象的方法缓存以及方法列表中查找方法实现
- 尝试在父类对象的方法缓存以及方法列表中查找方法实现
- 如果没有找到方法实现,则尝试进行动态方法解析
- 如果动态方法解析失败,则尝试进行消息转发
- 解锁、返回方法实现
// path: objc4-756.2/runtime/objc-runtime-new.mm /* 标准的方法实现(imp)查找函数 当参数 initialize == NO 时,会试图避免调用当前类 cls 的 +initialize 方法(但是有时会失败) 当参数 cache == NO 时,会跳过当前类 cls 方法缓存的查找(但是会在其他地方使用方法缓存) 大多数调用者应该使用(initialize == YES && cache == YES)调用本函数 inst 是当前类 cls 或其子类的一个实例。如果 inst 未知,则可以传 nil 如果 cls 是一个未初始化的元类,那么非空的 inst 会使方法实现的查找更快 可能会返回 _objc_msgForward_impcache 函数的查询结果 用于外部使用的方法实现(imp),必须转换为 _objc_msgForward 或者 _objc_msgForward_stret 如果不想进行消息转发,请使用 lookUpImpOrNil 函数替代本函数 param.cls 要查找方法实现的类对象 param.sel 要查找的方法实现对应的方法选择器 param.inst 当前类 cls 或其子类的一个实例对象。如果 inst 未知,则可以传 nil。 param.initialize 是否调用当前类 cls 的 +initialize 方法 param.cache 是否查找当前类 cls 的方法缓存 param.resolver 方法实现未找到时,是否尝试动态方法解析 return 方法选择器 sel 对应的方法实现 */ IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver) { IMP imp = nil; bool triedResolver = NO; // 1.在当前类对象中进行不加锁的方法缓存查找,以提高方法缓存的使用性能 // 因为在 objc_msgSend 函数中已经查找过当前类对象的方法缓存了 // 所以 _class_lookupMethodAndLoadCache3 函数传入的参数 cache == NO // 即这里会直接跳过 if 语句中代码的执行 runtimeLock.assertUnlocked(); if (cache) { imp = cache_getImp(cls, sel); if (imp) return imp; } // 2.加锁 && 检查当前类对象是否已知 // 2.1为什么需要加锁: // 2.1.1 在方法实现查找期间需要持有运行时锁,以使方法实现的查找(method-lookup)和方法缓存的填充(cache-fill)保持原子性 // 保证在运行以下代码时不会有新方法添加导致缓存被刷新(flush) // 2.1.2 在检查类对象是否实现与是否初始化期间需要持有运行时锁,以防止并发实现与并发初始化之间的竞争 runtimeLock.lock(); // 2.2检查当前类对象是否在(所有已知类对象的列表)中。如果当前类对象未知,则会出现致命错误 checkIsKnownClass(cls); // 3.如果当前类对象没有实现(isRealized)或者初始化(isInitialized),则实现或者初始化当前类对象 // 3.1如果当前类对象没有实现(isRealized),则实现当前类对象 if (!cls->isRealized()) { cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock); // 在调用 realizeClassMaybeSwiftAndLeaveLocked 函数时,运行时锁有可能会被删除。但是现在运行时锁已经重新锁定 } // 3.2如果当前类对象没有初始化(isInitialized),则初始化当前类对象 if (initialize && !cls->isInitialized()) { cls = initializeAndLeaveLocked(cls, inst, runtimeLock); // 在调用 initializeAndLeaveLocked 函数时,运行时锁有可能会被删除。但是现在运行时锁已经重新锁定 // 如果参数 sel == initialize,则类对象 cls 的 +initialize 方法会被调用 2 次: // 1.initializeNonMetaClass 函数会调用类对象的 +initialize 方法 // 2.在此过程执行完成之后,类对象将再次调用自己的 +initialize 方法 // 当然,如果不是类对象主动调用自己的 +initialize 方法,则以上情况就不会发生 } retry: // 断言:运行时锁处于锁定状态 runtimeLock.assertLocked(); // 4.在当前类对象的方法缓存以及方法列表中查找方法实现 // 4.1尝试在当前类对象的方法缓存中查找方法实现 // C++ 函数 cache_getImp 对应汇编函数 _cache_getImp,汇编函数 _cache_getImp 本质上也是调用汇编函数 CacheLookup 在当前类对象的方法缓存中查找方法实现 imp = cache_getImp(cls, sel); if (imp) goto done; // 4.2尝试在当前类对象的方法列表中查找方法实现 { // getMethodNoSuper_nolock 函数用于在当前类对象 cls 的方法列表数组中搜索方法名称 sel 所标识的方法 Method meth = getMethodNoSuper_nolock(cls, sel); if (meth) { // 如果在类对象 cls 的方法列表数组中找到了 sel 所标识的方法,则将相应的方法实现添加到类对象 cls 的方法缓存中 log_and_fill_cache(cls, meth->imp, sel, inst, cls); imp = meth->imp; goto done; } } // 5.尝试在父类对象的方法缓存以及方法列表中查找方法实现 { // (在父类对象中查找方法实现的思路)基本上与(在类对象中查找方法实现的思路)是一样的,只是多了个循环用来遍历继承链 unsigned attempts = unreasonableClassCount(); for (Class curClass = cls->superclass; curClass != nil; curClass = curClass->superclass) { // 如果继承链中有循环引用,则停止遍历继承链(类列表中的内存被污染了) if (--attempts == 0) { _objc_fatal("Memory corruption in class list."); } // 5.1尝试在父类对象的方法缓存中查找方法实现 imp = cache_getImp(curClass, sel); if (imp) { if (imp != (IMP)_objc_msgForward_impcache) { // 如果在父类对象 curClass 的方法缓存中找到了 sel 所标识的方法,则将相应的方法实现添加到当前类对象 cls 的方法缓存中 log_and_fill_cache(cls, imp, sel, inst, curClass); goto done; } else { // 如果在父类的方法缓存中找到的方法实现(imp)是 _objc_msgForward_impcache,则结束继承链的遍历,开始执行动态方法解析 break; } } // 5.2尝试在父类对象的方法列表数组中查找方法实现 Method meth = getMethodNoSuper_nolock(curClass, sel); if (meth) { // 如果在父类对象 curClass 的方法列表数组中找到了 sel 所标识的方法,则将相应的方法实现添加到当前类对象 cls 的方法缓存中 log_and_fill_cache(cls, meth->imp, sel, inst, curClass); imp = meth->imp; goto done; } } } // 6.如果没有找到方法实现,则尝试进行动态方法解析 if (resolver && !triedResolver) { // 因为程序在尝试进行动态方法解析期间没有持有运行时锁,方法缓存有可能在此期间已经改变 // 所以这里没有直接获取动态解析出来的方法实现进行缓存,而是从头开始再执行一遍方法实现的查找流程 runtimeLock.unlock(); resolveMethod(cls, sel, inst); runtimeLock.lock(); // 标志位 triedResolver 用于控制动态方法解析只会执行一次 triedResolver = YES; // 重启方法实现的查找流程 goto retry; } // 7.如果动态方法解析失败,则尝试进行消息转发 // (IMP)_objc_msgForward_impcache 对应的汇编函数为 __objc_msgForward_impcache imp = (IMP)_objc_msgForward_impcache; // 如果在进行消息转发的过程中找到了 sel 所标识的方法,则将相应的方法实现 imp 添加到当前类对象 cls 的方法缓存中 cache_fill(cls, sel, imp, inst); done: // 8.解锁、返回方法实现 runtimeLock.unlock(); return imp; }
lookUpImpOrForward
函数的前半部分用于:在消息接受者所属的类对象的方法列表,以及父类对象的方法缓存和方法列表中,查找方法实现。即用于在 消息发送阶段 执行 慢速查找方法实现 的流程lookUpImpOrForward
函数的后半部分用于:执行 消息转发阶段 相关的逻辑关于 RunTime 如何进行:方法列表的查找 && 方法缓存的填充,将在整个 消息机制 的主流程分析完成之后单独讲解。现在我们要做的是趁热打铁,把握住主流程,紧接着进行 消息转发阶段 相关的逻辑的分析
消息转发阶段
-
消息转发阶段:动态方法解析
lookUpImpOrForward
函数的第 6 步用于进行动态方法解析,而resolveMethod
函数为动态方法解析的入口:// path: objc4-756.2/runtime/objc-runtime-new.mm // resolveMethod 函数用于判断参数 cls 是(类对象)还是(元类对象): // 1.如果参数 cls 是类对象(对象方法存储在类对象中),则执行对象方法的动态解析(resolveInstanceMethod) // 2.如果参数 cls 是元类对象(类方法存储在元类对象中),则执行类方法的动态解析(resolveClassMethod) static void resolveMethod(Class cls, SEL sel, id inst) { // 断言:未加锁(runtimeLock) runtimeLock.assertUnlocked(); // 断言:类对象 cls 已经实现 assert(cls->isRealized()); if (! cls->isMetaClass()) { // 1.如果参数 cls 是类对象(对象方法存储在类对象中),则执行 resolveInstanceMethod 函数进行对象方法的动态解析 resolveInstanceMethod(cls, sel, inst); } else { // 2.如果参数 cls 是元类对象(类方法存储在元类对象中),则执行 resolveClassMethod 函数进行类方法的动态解析 resolveClassMethod(cls, sel, inst); // 因为根元类对象的父类是根类对象 // 所以当类方法找不到时,会搜索 NSObject 中同名的对象方法 if (!lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) { resolveInstanceMethod(cls, sel, inst); } } }
在
resolveMethod
函数中,如果是进行对象方法的动态解析,则调用resolveInstanceMethod
函数:// path: objc4-756.2/runtime/objc-runtime-new.mm // resolveInstanceMethod 函数用于调用当前类对象 cls 的 +resolveInstanceMethod 方法 static void resolveInstanceMethod(Class cls, SEL sel, id inst) { // 断言:未加锁(runtimeLock) runtimeLock.assertUnlocked(); // 断言:类对象 cls 已经实现 assert(cls->isRealized()); // 查看当前类 cls 是否实现了 +resolveInstanceMethod 方法 // 如果当前类 cls 未实现 +resolveInstanceMethod 方法,则直接返回 if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) { return; } // 将 objc_msgSend 函数强制转换成与 +resolveInstanceMethod 方法类型相同的函数指针 msg BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend; // 向当前类 cls 发送 resolveInstanceMethod 消息,即调用当前类 cls 的 +resolveInstanceMethod 方法 // 第三个参数 sel 用于标识未识别的方法名称 // 因为当前类 cls 的 +resolveInstanceMethod 方法的返回值 resolved 只是用于标识是否进行日志输出 // 所以无论当前类 cls 的 +resolveInstanceMethod 方法返回 YES 还是返回 NO,对执行流程几乎没有影响 bool resolved = msg(cls, SEL_resolveInstanceMethod, sel); // 查看当前类 cls 是否已经通过 +resolveInstanceMethod 方法添加了方法名称 sel 对应的方法实现 IMP imp = lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/); // 进行日志输出 if (resolved && PrintResolving) { if (imp) { _objc_inform("RESOLVE: method %c[%s %s] " "dynamically resolved to %p", cls->isMetaClass() ? '+' : '-', cls->nameForLogging(), sel_getName(sel), imp); } else { _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES" ", but no new implementation of %c[%s %s] was found", cls->nameForLogging(), sel_getName(sel), cls->isMetaClass() ? '+' : '-', cls->nameForLogging(), sel_getName(sel)); } } }
在
resolveMethod
函数中,如果是进行类方法的动态解析,则调用resolveClassMethod
函数:// path: objc4-756.2/runtime/objc-runtime-new.mm // resolveClassMethod 函数用于调用当前类对象 cls 的 +resolveClassMethod 方法 static void resolveClassMethod(Class cls, SEL sel, id inst) { // 断言:未加锁(runtimeLock) runtimeLock.assertUnlocked(); // 断言:元类对象 cls 已经实现 assert(cls->isRealized()); // 断言:元类对象 cls 是元类 assert(cls->isMetaClass()); // 查看当前类 cls 是否实现了 +resolveClassMethod 方法 // 如果当前类 cls 未实现 +resolveClassMethod 方法,则直接返回 if (! lookUpImpOrNil(cls, SEL_resolveClassMethod, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) { return; } Class nonmeta; { mutex_locker_t lock(runtimeLock); nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst); // +initialize path should have realized nonmeta already if (!nonmeta->isRealized()) { _objc_fatal("nonmeta class %s (%p) unexpectedly not realized", nonmeta->nameForLogging(), nonmeta); } } // 将 objc_msgSend 函数强制转换成与 +resolveClassMethod 方法类型相同的函数指针 msg BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend; // 向当前类 cls 发送 resolveClassMethod 消息,即调用当前类 cls 的 +resolveClassMethod 方法 // 第三个参数 sel 用于标识未识别的方法名称 // 因为当前类 cls 的 +resolveClassMethod 方法的返回值 resolved 只是用于标识是否进行日志输出 // 所以无论当前类 cls 的 +resolveClassMethod 方法返回 YES 还是返回 NO,对执行流程几乎没有影响 bool resolved = msg(nonmeta, SEL_resolveClassMethod, sel); // 查看当前类 cls 是否已经通过 +resolveClassMethod 方法添加了方法名称 sel 对应的方法实现 IMP imp = lookUpImpOrNil(cls, sel, inst, NO/*initialize*/, YES/*cache*/, NO/*resolver*/); // 进行日志输出 if (resolved && PrintResolving) { if (imp) { _objc_inform("RESOLVE: method %c[%s %s] " "dynamically resolved to %p", cls->isMetaClass() ? '+' : '-', cls->nameForLogging(), sel_getName(sel), imp); } else { _objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES" ", but no new implementation of %c[%s %s] was found", cls->nameForLogging(), sel_getName(sel), cls->isMetaClass() ? '+' : '-', cls->nameForLogging(), sel_getName(sel)); } } }
-
消息转发阶段:消息接受者重定向 && 完整的消息重定向
lookUpImpOrForward
函数的第 7 步用于进行(消息接受者重定向 && 完整的消息重定向),而函数指针_objc_msgForward_impcache
为(消息接受者重定向 && 完整的消息重定向)的入口:// path: objc4-756.2/runtime/objc-msg-arm64.s STATIC_ENTRY __objc_msgForward_impcache // 跳转到 __objc_msgForward 处执行 b __objc_msgForward END_ENTRY __objc_msgForward_impcache
汇编函数
__objc_msgForward_impcache
的实现特别的简单,其内部仅仅是调用了汇编函数__objc_msgForward
:// path: objc4-756.2/runtime/objc-msg-arm64.s ENTRY __objc_msgForward // 这两条指令用于将函数指针 _objc_forward_handler 加载到 x17 中 adrp x17, __objc_forward_handler@PAGE ldr p17, [x17, __objc_forward_handler@PAGEOFF] // 这条指令用于跳转到 x17 所指向的位置,相当于 br x17。即执行函数指针 _objc_forward_handler 指向的函数 TailCallFunctionPointer x17 END_ENTRY __objc_msgForward
在汇编函数
__objc_msgForward
中,加载并执行了函数指针_objc_forward_handler
:// path: objc4-756.2/runtime/objc-runtime.mm // _objc_forward_handler 的默认值 __attribute__((noreturn)) void objc_defaultForwardHandler(id self, SEL sel) { _objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p " "(no message forward handler is installed)", class_isMetaClass(object_getClass(self)) ? '+' : '-', object_getClassName(self), sel_getName(sel), self); } void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
函数指针
_objc_forward_handler
默认指向objc_defaultForwardHandler
函数,而objc_defaultForwardHandler
函数的实现非常简单:在输出日志后停止该进程咦~?不是说好了要进行消息转发的吗?怎么到这里就崩溃了?不要着急,我们看一下编译器指令
__attribute__((noreturn))
编译器指令__attribute__((noreturn))
用于告诉编译器此函数不会返回给调用者,以便编译器在优化时去掉不必要的代码
什么意思呢?objc_defaultForwardHandler
函数只是函数指针_objc_forward_handler
的默认值。如果想要实现消息转发,则需要替换函数指针_objc_forward_handler
的默认值而
objc_setForwardHandler
函数正是用于替换函数指针_objc_forward_handler
的默认值:// path: objc4-756.2/runtime/objc-runtime.mm // objc_setForwardHandler 函数用于设置函数指针 _objc_forward_handler 和 _objc_forward_stret_handler void objc_setForwardHandler(void *fwd, void *fwd_stret) { _objc_forward_handler = fwd; #if SUPPORT_STRET _objc_forward_stret_handler = fwd_stret; #endif }
换句话说,只要找到
objc_setForwardHandler
函数的调用者,就能知道是谁执行了消息转发的流程那么是谁调用了
objc_setForwardHandler
函数呢?
通过符号断点与跟踪函数调用栈,可以确定是CoreFoundation.__CFInitialize
函数调用了objc_setForwardHandler
函数,并将CoreFoundation._CF_forwarding_prep_0
函数作为参数传递给了fwd
,即fwd == _objc_forward_handler == _CF_forwarding_prep_0
而在
_CF_forwarding_prep_0
函数内部,则调用了CoreFoundation.___forwarding___
函数执行真正的消息转发。很遗憾这一部分的代码并没有开源,但是有人根据汇编代码复原了___forwarding___
函数的实现// path: Foundation.framework // __forwarding__ 函数用于执行真正的消息转发流程 // param.frameStackPointer 被转发的消息的栈指针 // param.isStret 是否返回结构体 int __forwarding__(void *frameStackPointer, int isStret) { // 1.准备参数 id receiver = *(id *)frameStackPointer; // 根据栈指针 frameStackPointer 获取消息的接受者(即方法的调用者):self SEL sel = *(SEL *)(frameStackPointer + 8); // 根据栈指针 frameStackPointer 获取消息的方法选择器:_cmd const char *selName = sel_getName(sel); // 获取消息的名称 Class receiverClass = object_getClass(receiver); // 获取消息接受者所属的类对象 // 2.消息接受者重定向 // 调用当前类对象 receiverClass 的 forwardingTargetForSelector: 方法 if (class_respondsToSelector(receiverClass, @selector(forwardingTargetForSelector:))) { // 获取备用的消息接收者(即获取备用的方法调用者) id forwardingTarget = [receiver forwardingTargetForSelector:sel]; // 备用的消息接受者不能为空 && 备用的消息接受者不能和当前消息接受者是同一个对象 if (forwardingTarget && forwardingTarget != receiver) { // 根据消息的返回值是否为结构体,调用不同的 objc_msgSend 函数进行消息的发送 if (isStret == 1) { int ret; objc_msgSend_stret(&ret,forwardingTarget, sel, ...); return ret; } return objc_msgSend(forwardingTarget, sel, ...); } } // 3.检测当前类对象 receiverClass 是否为僵尸对象 const char *className = class_getName(receiverClass); const char *zombiePrefix = "_NSZombie_"; size_t prefixLen = strlen(zombiePrefix); if (strncmp(className, zombiePrefix, prefixLen) == 0) { CFLog(kCFLogLevelError, @"*** -[%s %s]: message sent to deallocated instance %p", className + prefixLen, selName, receiver); } // 4.完整的消息重定向 // 4.1调用当前类对象 receiverClass 的 methodSignatureForSelector: 方法获取方法签名 if (class_respondsToSelector(receiverClass, @selector(methodSignatureForSelector:))) { // 获取方法签名 NSMethodSignature *methodSignature = [receiver methodSignatureForSelector:sel]; // 方法签名不能为空 if (methodSignature) { // 检测(由 methodSignature 所标识的返回值类型)与(由参数 isStret 所标识的返回值类型)是否相匹配 BOOL signatureIsStret = [methodSignature _frameDescriptor]->returnArgInfo.flags.isStruct; if (signatureIsStret != isStret) { CFLog(kCFLogLevelWarning , @"*** NSForwarding: warning: method signature and compiler disagree on struct-return-edness of '%s'. Signature thinks it does%s return a struct, and compiler thinks it does%s.", selName, signatureIsStret ? "" : not, isStret ? "" : not); } // 4.2调用当前类对象 receiverClass 的 forwardInvocation: 方法进行消息转发 if (class_respondsToSelector(receiverClass, @selector(forwardInvocation:))) { // 由 NSMethodSignature 生成 NSInvocation,并进行调用 NSInvocation *invocation = [NSInvocation _invocationWithMethodSignature:methodSignature frame:frameStackPointer]; [receiver forwardInvocation:invocation]; // 从 NSInvocation 中获取返回值并返回 void *returnValue = NULL; [invocation getReturnValue:&value]; return returnValue; } else { CFLog(kCFLogLevelWarning , @"*** NSForwarding: warning: object %p of class '%s' does not implement forwardInvocation: -- dropping message", receiver, className); return 0; } } } // 4.3调用当前类对象 receiverClass 的 doesNotRecognizeSelector: 方法以结束进程 SEL *registeredSel = sel_getUid(selName); if (sel != registeredSel) { CFLog(kCFLogLevelWarning , @"*** NSForwarding: warning: selector (%p) for message '%s' does not match selector known to Objective C runtime (%p)-- abort", sel, selName, registeredSel); } else if (class_respondsToSelector(receiverClass, @selector(doesNotRecognizeSelector:))) { [receiver doesNotRecognizeSelector:sel]; } else { CFLog(kCFLogLevelWarning , @"*** NSForwarding: warning: object %p of class '%s' does not implement doesNotRecognizeSelector: -- abort", receiver, className); } // 5.如果当前类对象 receiverClass 没有实现 doesNotRecognizeSelector: 方法,则直接杀死进程 // 如果当前类对象 receiverClass 实现了 doesNotRecognizeSelector: 方法,则调用 doesNotRecognizeSelector: 方法后杀死进程 kill(getpid(), 9); }
补充:方法列表的查找 && 方法缓存的填充
在分析完了消息机制的主流程后,我们回过头来分析消息机制中消息发送阶段的两个分支流程:
① 方法列表的查找
② 方法缓存的填充
-
① 方法列表的查找
// path: objc4-756.2/runtime/objc-runtime-new.mm // getMethodNoSuper_nolock 函数用于在当前类对象 cls 的方法列表数组中搜索方法名称 sel 所标识的方法 static method_t * getMethodNoSuper_nolock(Class cls, SEL sel) { // 断言:已经加锁(runtimeLock) runtimeLock.assertLocked(); // 断言:当前类对象 cls 已经实现 assert(cls->isRealized()); // 方法列表数组是一个二维数组,里面存储着一个一个的方法列表 for (auto mlists = cls->data()->methods.beginLists(), end = cls->data()->methods.endLists(); mlists != end; ++mlists) { // 取出存储在方法列表数组中的方法列表,在方法列表中搜索方法名称 sel 所标识的方法 method_t *m = search_method_list(*mlists, sel); if (m) return m; } // 如果在当前类对象 cls 的方法列表数组中没有找到方法名称 sel 所标识的方法,则返回 nil return nil; }
// path: objc4-756.2/runtime/objc-runtime-new.mm // search_method_list 函数用于在单个方法列表 mlist 中搜索方法名称 sel 所标识的方法 static method_t * search_method_list(const method_list_t *mlist, SEL sel) { int methodListIsFixedUp = mlist->isFixedUp(); int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t); if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) { // 对已排序的方法列表进行二分查找 return findMethodInSortedMethodList(sel, mlist); } else { // 对未排序的方法列表进行线性查找(即遍历查找) for (auto& meth : *mlist) { if (meth.name == sel) return &meth; } } #if DEBUG // sanity-check negative results // 在 debug 模式下,有可能出现: // 方法列表 mlist 的排序标志位 isFixedUp == YES,但是实际上方法列表 mlist 并没有完全按照 sel 进行排序的情况 // 此时应该理性地对方法列表 mlist 进行遍历查找,以确定方法列表 mlist 中确实没有包含方法名称 sel 所标识的方法 if (mlist->isFixedUp()) { for (auto& meth : *mlist) { if (meth.name == sel) { _objc_fatal("linear search worked when binary search did not"); } } } #endif // 如果在方法列表 mlist 中没有找到方法名称 sel 所标识的方法,则返回 nil return nil; }
// path: objc4-756.2/runtime/objc-runtime-new.mm // findMethodInSortedMethodList 函数用于对已排序的方法列表进行二分查找 static method_t * findMethodInSortedMethodList(SEL key, const method_list_t *list) { // 断言:方法列表 list 不为空 assert(list); const method_t * const first = &list->first; // 获取方法列表 list 首地址上的元素(即第一个元素)给变量 first const method_t *base = first; // base 为二分查找的左起点。二分查找开始时,二分查找的左起点 == 方法列表的第一个元素 const method_t *probe; // probe 为二分查找的游标(也即二分查找的右终点) uintptr_t keyValue = (uintptr_t)key; // 将 SEL 类型的目标方法的名称 key 强制转换为 uintptr_t 类型的位置索引 keyValue uint32_t count; // 方法列表中方法的数量 // 从方法列表的最右边开始,只要左起点与右终点没有重合,方法列表中方法的数量 count 就右移一位(也就是缩小一半取整) for (count = list->count; count != 0; count >>= 1) { // 1.如果目标方法的位置在游标 probe 的左边 // 游标(probe) == 二分查找左起点(base) + 现有方法列表中方法的数量除以二(count >> 1) probe = base + (count >> 1); // 获取游标处方法的名称 uintptr_t probeValue = (uintptr_t)probe->name; // 2.如果找到目标方法 if (keyValue == probeValue) { // 方法列表 list 在排序函数 fixupMethodList 中已经使用 std::stable_sort 进行文档排序(按照 Selector 递增排序),这确保了分类的同名方法会排在方法列表 list 的左边 // 为了确保分类的同名方法会优先被调用,在找到目标方法的位置之后,还会继续向左查找是否有名称相同的方法。最后找到的那个方法,才是最终要调用的方法 while (probe > first && keyValue == (uintptr_t)probe[-1].name) { probe--; } // 返回找到的目标方法 return (method_t *)probe; } // 3.如果目标方法的位置在游标 probe 的右边 if (keyValue > probeValue) { // 将二分查找的左起点 base 设置为游标 probe 右侧那个方法的地址 // 对应地,方法列表中方法的数量也要 - 1 base = probe + 1; count--; } } return nil; }
-
② 方法缓存的填充
// path: objc4-756.2/runtime/objc-runtime-new.mm // log_and_fill_cache 函数用于:进行方法调用的日志输出 && 进行方法缓存的填充 static void log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer) { #if SUPPORT_MESSAGE_LOGGING // 1.进行方法调用的日志输出 if (objcMsgLogEnabled) { bool cacheIt = logMessageSend(implementer->isMetaClass(), cls->nameForLogging(), implementer->nameForLogging(), sel); if (!cacheIt) return; } #endif // 2.进行方法缓存的填充 cache_fill (cls, sel, imp, receiver); }
// path: objc4-756.2/runtime/objc-cache.mm // cache_fill 函数用于:加锁 && 进行方法缓存的填充 void cache_fill(Class cls, SEL sel, IMP imp, id receiver) { #if !DEBUG_TASK_THREADS // 1.加锁(cacheUpdateLock) mutex_locker_t lock(cacheUpdateLock); // 2.进行方法缓存的填充 cache_fill_nolock(cls, sel, imp, receiver); #else // 收集关键信息 _collecting_in_critical(); return; #endif }
// path: objc4-756.2/runtime/objc-cache.mm // cache_fill_nolock 函数用于进行方法缓存的填充 static void cache_fill_nolock(Class cls, SEL sel, IMP imp, id receiver) { // 1.断言:已经加锁(cacheUpdateLock) cacheUpdateLock.assertLocked(); // 2.进行安全性判断 // 2.1不能在当前类对象 cls 执行 +initialize 前进行方法缓存 if (!cls->isInitialized()) return; // 2.2确保在获得锁(cacheUpdateLock)之前,sel-imp 没有被其他线程添加到方法缓存散列表(cache_t.buckets)中 if (cache_getImp(cls, sel)) return; // 3.取出当前类对象 cls 中的方法缓存(cahce_t) cache_t *cache = getCache(cls); // 4.判断当前类对象 cls 中的方法缓存散列表(cahce_t.buckets)是否需要扩容 // 4.1当前方法缓存(cache_t)中已用容量 occupied + 1 得到 newOccupied mask_t newOccupied = cache->occupied() + 1; // 4.2.获取当前方法缓存(cache_t)的总容量 mask_t capacity = cache->capacity(); // 4.3对当前方法缓存(cache_t)的总容量和已用容量进行比较: if (cache->isConstantEmptyCache()) { // 4.3.1如果方法缓存散列表(cache_t.buckets)是空的,则调用 reallocate 函数去创建新的方法缓存散列表(cache_t.buckets)并赋值给当前方法缓存(cache_t) cache->reallocate(capacity, capacity ?: INIT_CACHE_SIZE); } else if (newOccupied <= capacity / 4 * 3) { // 4.3.2如果方法缓存散列表(cache_t.buckets)的已用容量 <= 总容量的 3/4 ,则不做任何操作 } else { // 4.3.3如果方法缓存散列表(cache_t.buckets)的已用容量 > 总容量的 3/4 ,则进行扩容 cache->expand(); } // 5.扫描方法缓存散列表(cache_t.buckets)获取第一个未使用的空槽(bucket_t),并将 sel-imp 插入到此空槽(bucket_t) // 因为方法缓存散列表(cache_t.buckets)的最小容量为 4,并且当已用容量达到总容量的 3/4 时会进行扩容 // 所以一定能找到一个空槽(bucket_t) bucket_t *bucket = cache->find(sel, receiver); // incrementOccupied == _occupied++; if (bucket->sel() == 0) cache->incrementOccupied(); bucket->set<Atomic>(sel, imp); }
以下是方法缓存填充过程中用到的几个辅助函数:
// path: objc4-756.2/runtime/objc-cache.mm // expand 函数用于将方法缓存散列表(cache_t.buckets)的容量扩充 1 倍 void cache_t::expand() { // 断言:已经加锁(cacheUpdateLock) cacheUpdateLock.assertLocked(); // 获取方法缓存散列表旧的总容量 uint32_t oldCapacity = capacity(); // 方法散列表新的总容量 = 旧的总容量 * 2 // 注意:INIT_CACHE_SIZE == 4 uint32_t newCapacity = oldCapacity ? oldCapacity*2 : INIT_CACHE_SIZE; // 处理方法缓存散列表总容量溢出的情况 if ((uint32_t)(mask_t)newCapacity != newCapacity) { // mask overflow - can't grow further // fixme this wastes one bit of mask newCapacity = oldCapacity; } // 为方法缓存散列表重新分配内存空间(会清空方法缓存散列表的所有缓存) // 创建新的方法缓存散列表 && 释放旧的方法缓存散列表 // 注意: // 新创建的方法缓存散列表中不存储任何方法缓存(包括之前的方法缓存) // 即创建新的方法缓存散列表后,会清空原先方法缓存散列表中的所有缓存 reallocate(oldCapacity, newCapacity); }
// path: objc4-756.2/runtime/objc-cache.mm // reallocate 函数用于:创建新的方法缓存散列表(cache_t.buckets) && 释放旧的方法缓存散列表(cache_t.buckets) // 注意: // 新创建的方法缓存散列表(cache_t.buckets)中不存储任何方法缓存(包括之前的方法缓存) // 即创建新的方法缓存散列表后,会清空原先方法缓存散列表中的所有缓存 // 因为在 reallocate 后方法缓存散列表(cache_t.buckets)的总容量 capacity 和掩码 mask 将会发生改变 // 又因为同一个方法名称 sel 在不同的掩码 mask 下生成的索引是不同的,即同一个方法在不同容量的方法缓存散列表中的存储位置是不同的 // 所以出于性能考虑,新创建的方法缓存散列表不会存储任何缓存 void cache_t::reallocate(mask_t oldCapacity, mask_t newCapacity) { bool freeOld = canBeFreed(); // 获取原来的方法缓存散列表(cache_t.buckets) bucket_t *oldBuckets = buckets(); // 创建新的方法缓存散列表(cache_t.buckets) bucket_t *newBuckets = allocateBuckets(newCapacity); assert(newCapacity > 0); assert((uintptr_t)(mask_t)(newCapacity-1) == newCapacity-1); // 为当前方法缓存(cache_t)设置新的方法缓存散列表(newBuckets)与新的 mask(newCapacity - 1),并将新的方法缓存的 _occupied 置零 // 注意: // 新创建的方法缓存散列表(cache_t.buckets)中不存储任何方法缓存(包括之前的方法缓存) // 即创建新的方法缓存散列表后,会清空原先方法缓存散列表中的所有缓存 setBucketsAndMask(newBuckets, newCapacity - 1); // 释放原来的方法缓存散列表(cache_t.buckets)所占用的内存空间 if (freeOld) { cache_collect_free(oldBuckets, oldCapacity); cache_collect(false); } }
// path: objc4-756.2/runtime/objc-cache.mm // find 函数用于在当前方法缓存(cache_t)中根据方法名称 s 遍历查找方法缓存散列表(cache_t.buckets),返回用于存储方法名称 s 所标识的方法的槽(bucket_t) bucket_t * cache_t::find(SEL s, id receiver) { assert(s != 0); // 获取当前方法缓存的散列表(cache_t.buckets) bucket_t *b = buckets(); // 获取当前方法缓存的掩码(cache_t.mask) mask_t m = mask(); // 通过 cache_hash 函数计算出参数 s 对应的索引值(begin = s & m),用来作为记录查询的起始索引 mask_t begin = cache_hash(s, m); // 将查询的起始索引赋值给变量 i(变量 i 用于切换索引) mask_t i = begin; do { // 用索引 i 从方法缓存散列表(cache_t.buckets)中取值 // 如果取出来的 bucket_t 的 key == s,则说明查询成功,返回该 bucket_t // 如果取出来的 bucket_t 的 key == 0,则说明在索引 i 的位置上还没有缓存过方法,同样需要返回该 bucket_t,用于中止缓存查询 if (b[i].sel() == 0 || b[i].sel() == s) { return &b[i]; } // 在 arm64 架构下执行倒序遍历(i = i ? i-1 : m) } while ((i = cache_next(i, m)) != begin); // 如果此时还没有找到参数 s 对应的 bucket_t,或者是空的 bucket_t,则说明查找失败,调用 bad_cache 函数 Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache)); cache_t::bad_cache(receiver, (SEL)s, cls); }
// path: objc4-756.2/runtime/objc-cache.mm // bad_cache 函数用于:记录异常信息 && 抛出异常 void cache_t::bad_cache(id receiver, SEL sel, Class isa) { // 单步记录日志,以防止日志本身导致奔溃 _objc_inform_now_and_on_crash ("Method cache corrupted. This may be a message to an " "invalid object, or a memory error somewhere else."); cache_t *cache = &isa->cache; _objc_inform_now_and_on_crash ("%s %p, SEL %p, isa %p, cache %p, buckets %p, " "mask 0x%x, occupied 0x%x", receiver ? "receiver" : "unused", receiver, sel, isa, cache, cache->_buckets, cache->_mask, cache->_occupied); _objc_inform_now_and_on_crash ("%s %zu bytes, buckets %zu bytes", receiver ? "receiver" : "unused", malloc_size(receiver), malloc_size(cache->_buckets)); _objc_inform_now_and_on_crash ("selector '%s'", sel_getName(sel)); _objc_inform_now_and_on_crash ("isa '%s'", isa->nameForLogging()); _objc_fatal ("Method cache corrupted. This may be a message to an " "invalid object, or a memory error somewhere else."); }
补充:消息机制底层的一些细节
-
Objective-C 允许一个
nil
对象执行任意方法而不会 Crash。因为在objc_msgSend
的汇编源码中,当方法调用者(即消息接受者)为nil
时,objc_msgSend
会直接跳过消息的发送流程并返回一个空值 -
objc_msgSend
使用汇编实现的原因:- 所有 Objective-C 的方法调用最终都会被转换成消息发送,这导致
objc_msgSend
的调用频率非常的高,为了最大限度地提升代码性能,苹果采用了更贴近硬件底层的汇编语言来实现objc_msgSend
- 在实际的开发过程中,方法调用者(即消息接受者)的返回值千奇百怪:有的返回一个对象类型的指针,有的返回一个整数,有的返回一个浮点数,…,有的什么也没有返回。为了适应花样繁多的返回值,在
void objc_msgSend(void /* id self, SEL op, ... */ )
的函数声明中有这么一句注释:当使用此函数进行消息发送时,需要将此函数强制转换成适当的函数指针类型
如果用 C、C++ 等高级语言来实现objc_msgSend
,则会出现这么一个问题:objc_msgSend
明明声明了一个 void 类型的返回值,但是却在代码实现中返回了一个非 void 类型的返回值,这在 C、C++ 等高级语言的语法上是不被允许的!!!
- 所有 Objective-C 的方法调用最终都会被转换成消息发送,这导致
-
为什么
objc_msgSend
的参数列表和imp
的参数列表是一样的?假设 Person 类有对象方法如下:
-(void)eatFood:(NSString *)aFood inPlace:(NSString *)aPlace;
编译后,其对应的 C 函数如下:
static void _I_Person_eatFood_inPlace_(Person * self, SEL _cmd, NSString *aFood, NSString *aPlace);
函数
_I_Person_eatFood_inPlace_
对应的函数指针 imp 如下:IMP imp = _I_Person_eatFood_inPlace_;
调用 Person 类对象方法
-eatFood:inPlace:
的两种方式如下:objc_msgSend(aPerson, @selector(eatFood:inPlace:), @"hamburger", @"MacDonald's"); imp(aPerson, @selector(eatFood:inPlace:), @"hamburger", @"MacDonald's");
RunTime 消息机制的核心目的是根据方法名称(sel)寻找对应的方法实现(imp),并跳转到该方法实现(imp)处去执行
换句话说:objc_msgSend
最终会调用函数指针 imp,所以objc_msgSend
的参数列表必须包含函数指针 imp 所需的所有参数
还需要注意一点:使用objc_msgSend
需要经过消息发送的流程才会最终调用方法实现,而使用 imp 则是跳过消息发送的流程直接调用方法的实现
在某些对性能要求极为严苛的场合下,可以使用函数指针 imp 跳过消息发送的流程直接调用方法的实现 -
objc_msgSend
函数是通过汇编语言实现的,以下是一个关于objc_msgSend
函数使用 RunTime API 的伪代码实现:id objc_msgSend(id self, SEL _cmd, ...) { Class cls = object_getClass(self); IMP imp = cache_lookup(cls, _cmd); if(!imp) imp = class_getMethodImplementation(cls, _cmd); return imp(self, _cmd, ...); }
-
class_getMethodImplementation
函数底层实现分析/* 获取给定类给定方法名称所对应的方法实现 @param cls 要检视的类 @param name 方法选择器,用于标识要获取方法实现的方法的名称 @return 给定类 cls 给定方法名称 name 所对应的方法实现。如果参数 cls 传 Nil,则返回 NULL @note class_getMethodImplementation() 的执行速度会比 method_getImplementation(class_getInstanceMethod(cls, name)) 快 @note 返回的函数指针可能是运行时内部的函数,而不是实际的方法实现 例如:如果类的实例对象没有响应方法选择器,则返回的函数指针将是运行时的消息转发机制的一部分 */ IMP class_getMethodImplementation(Class cls, SEL sel) { IMP imp; if (!cls || !sel) return nil; imp = lookUpImpOrNil(cls, sel, nil, YES/*initialize*/, YES/*cache*/, YES/*resolver*/); // 将消息转发函数转换成可调用的外部 C 版本 if (!imp) { return _objc_msgForward; } return imp; }
-
lookUpImpOrNil
函数与lookUpImpOrForward
函数的区别与联系在前面,我们已经详细分析了
lookUpImpOrForward
函数的底层实现。接下来,我们开始分析lookUpImpOrNil
函数的底层实现lookUpImpOrNil
函数的底层就是调用的lookUpImpOrForward
函数。但是lookUpImpOrNil
函数在方法实现未找到时会直接返回nil
,而不是像lookUpImpOrForward
函数那样返回_objc_msgForward_impcache
进入消息转发阶段。这一点,也可以从方法名称看出来:lookUpImpOrNil
:查找方法实现或者返回空lookUpImpOrForward
:查找方法实现或者转发
在实际开发过程中,当我们要获取一个方法的实现时,往往调用的是
lookUpImpOrNil
函数IMP lookUpImpOrNil(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver) { IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver); if (imp == _objc_msgForward_impcache) return nil; else return imp; }
-
为什么方法缓存散列表(
cache_t.buckets
)扩容之后,需要清空里面存储的方法缓存(bucket_t
)?因为在方法缓存散列表(
cache_t.buckets
)进行扩容时,会调用reallocate
函数创建一个容量翻倍的新的方法缓存散列表(cache_t.buckets
),并释放掉原有的方法缓存散列表(cache_t.buckets
)所占用的内存空间
又因为在通过reallocate
函数创建新的方法缓存散列表(cache_t.buckets
)后,方法缓存散列表(cache_t.buckets
)的总容量capacity
和掩码mask
将会发生改变
又因为同一个方法名称sel
在不同的掩码mask
下生成的索引是不同的,即同一个方法在不同容量的方法缓存散列表中的存储位置是不同的
所以出于性能考虑,新创建的方法缓存散列表(cache_t.buckets
)不会存储任何方法缓存(bucket_t
) -
在消息发送与消息转发的过程中,只要找到了 sel 所标识的方法,就会将相应的方法实现 imp 添加到当前类对象 cls 的方法缓存中:
- 如果在消息消息发送阶段(慢速查找方法实现)找到了 sel 所标识的方法,则会将相应的方法实现 imp 添加到当前类对象 cls 的方法缓存中
- 如果在消息转发阶段(动态方法解析、消息接受者重定向、完整的消息重定向)找到了 sel 所标识的方法,则会将相应的方法实现 imp 添加到当前类对象 cls 的方法缓存中
-
objc_msgSend 函数的 2 种使用方式
-(void)objc_msgSend_Demo { Person* aPerson = [[Person alloc] init]; NSString* aFood = @"hamburger"; NSString* aPlace = @"McDonald's"; // 如果直接调用 objc_msgSend 函数,则编译时会报错 Too many arguments to function call, expected 0, have 4 // 因为在 XCode 中默认对 objc_msgSend 函数的参数和返回值做严格的类型检查 // 而 objc_msgSend 被声明为一个无参数无返回值的函数 void objc_msgSend(void /* id self, SEL op, ... */ ); // objc_msgSend(aPerson, @selector(eatFood:inPlace:), aFood, aPlace); // ① 在调用之前,将 objc_msgSend 函数强制转换为适当的函数指针 NSString* (*msgSend)(id, SEL, NSString*, NSString*) = (typeof(msgSend))objc_msgSend; NSString* result0 = msgSend(aPerson, @selector(eatFood:inPlace:), aFood, aPlace); NSLog(@"result0 = %@", result0); // ② 修改工程配置,禁用对 objc_msgSend 函数的参数和返回值做严格的类型检查 // 将 Apple Clang - Preprocessing - Enable Strict Checking of objc_msgSend Callss 改为 NO NSString* result1 = objc_msgSend(aPerson, @selector(eatFood:inPlace:), aFood, aPlace); NSLog(@"result1 = %@", result1); }