Objective-C 的 RunTime(六):消息机制底层原理

RunTime 消息机制简介

在 Objective-C 中,方法的调用都是类似 [receiver selector]; 的形式,其本质是让对象在运行时动态地发送消息。我们来看看 receiver 对象调用 selector 方法时,在『编译阶段』和『运行阶段』分别会发生什么:

  • 编译阶段

    [receiver selector]; 方法调用被编译器转换为:

    1. objc_msgSend(receiver, selector);(不带参数)
    2. objc_msgSend(recevier, selector, org1, org2, …);(带参数)
  • 运行时:消息发送阶段

    消息接受者 recevier 寻找对应的 selector

    1. 通过 recevierisa 指针找到 recevier 对应的 Class(类)
    2. Class(类)的 Cache(方法缓存)的散列表中寻找对应的 IMP(方法实现)
    3. 如果在 Cache(方法缓存)中没有找到对应的 IMP(方法实现),则继续在 Class(类)的 Method List(方法列表)中找寻找对应的 selector。如果找到,则填充到 Cache(方法缓存)中,并返回 selector
    4. 如果在 Class(类)中没有找到这个 selector,则继续在 receviersuperclass(父类)中寻找
    5. 一旦找到对应的 selector,则直接执行 selector 对应的 IMP(方法实现)
    6. 如果找不到对应的 selector,则 RunTime 系统进入消息转发阶段
  • 运行时:消息转发阶段
    运行时:消息转发阶段
    ① 动态方法解析:通过重写当前 recevier+resolveInstanceMethod:(对象方法)或者 +resolveClassMethod:(类方法),利用 RunTime 的 class_addMethod 函数给当前 recevier 动态地添加其他方法实现

    ② 消息接受者重定向:如果上一步中没有添加其他方法实现,则可重写当前 recevier-forwardingTargetForSelector:(对象方法)或者 +forwardingTargetForSelector:(类方法),将消息动态地转发给其他对象处理

    ③ 完整的消息重定向:如果上一步的返回值为 nil 或者 self,则可重写当前 recevier-methodSignatureForSelector:(对象方法)或者 +methodSignatureForSelector:(类方法),获取消息的参数和返回值类型

    1. 如果 methodSignatureForSelector: 返回了一个 NSMethodSignature 对象(方法签名),则 RunTime 系统就会创建一个 NSInvocation 对象,并调用当前 recevier-forwardInvocation:(对象方法)或者 +forwardInvocation:(类方法),给予此次消息发送最后一次寻找 IMP(方法实现)的机会
    2. 如果 receviermethodSignatureForSelector: 返回 nil,则 RunTime 系统发出 -doesNotRecognizeSelector:(对象方法)或者 +doesNotRecognizeSelector:(类方法)消息,程序也就崩溃了
  • RunTime 消息机制的核心目的

    通过以上对 RunTime 消息机制的简单介绍,我们对 RunTime 消息机制有了总体上的印象

    接下来我们将以 RunTime 消息机制的入口函数 objc_msgSend 为起点,对 RunTime 消息机制的整个过程进行全面且详细的讲解。其中涉及到 arm 汇编和繁琐的函数调用,需要读者对 arm 汇编以及 RunTime 的基本数据结构有一定的了解

    因为 RunTime 消息机制中涉及到很多边界情况和意外情况的处理,所以初读本文可能会迷失在茫茫的细节中。在这里,我为读者竖起一座灯塔:RunTime 消息机制的核心目的是根据方法名称(sel)寻找对应的方法实现(imp),并跳转到该方法实现(imp)处去执行。 接下来介绍的所有汇编代码、函数调用、数据结构,都是围绕此核心目的展开,并为此核心目的服务。读者只要牢记此核心目的来阅读本文,就能加快对本文的理解

消息发送阶段

消息发送阶段的代码可以分为 2 部分:

  1. 快速查找方法实现。这一部分的代码用汇编实现,负责在消息接受者所属的类对象的方法缓存中查找方法实现

  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 函数主要负责的有:

    1. 获取消息接受者所属的类对象
    2. 获取类对象的方法缓存
    3. 通过方法名称(selector)在方法缓存中寻找方法实现(imp)
    4. 如果命中方法缓存,则跳转到对应的方法实现(imp)处去执行
    5. 如果没有命中方法缓存,则进入下一个部分(慢速查找方法实现)

    我们将在 arm64 架构下分析 objc_msgSend 函数的汇编代码,在开始分析之前我们需要了解:

    1. 在 arm64 架构的 cpu 中有 31 个 64 bit 的整数寄存器,分别被标记为 x0 - x30
    2. 每一个整数寄存器可以分离开来只使用低 32 bit,分别被标记为 w0 - w30
    3. 其中 x0 - x7 用于传递函数的参数,这意味着 objc_msgSend 函数接收的 self 参数被放在 x0 中,接收的 _cmd 参数被放在 x1 中
    4. 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 的代码可以看出:

    1. 开始查寻方法缓存时,都会从第一次获取到的索引位置开始倒序遍历方法缓存哈希表,到达表头之后返回表尾继续倒序遍历,直到(匹配成功)或者(查找的 bucket 为空)
    2. 当遍历方法缓存哈希表的流程返回到第一次获取到的索引的位置时,实际上整个方法缓存哈希表已经被遍历一遍了,并且表中的 bucket 都不为空 且 都不和 objc_msgSend 函数需要搜寻的目标 selector 匹配
    3. 当查寻再次环回到方法缓存哈希表的首部时,程序会跳转到 JumpMiss 处。当然,通常这种情况是不可能发生的,因为方法缓存哈希表足够大。所以只有当方法缓存哈希表被错误填满时,才有可能出现 JumpMiss 的情况。这个情况下我们需要跳转到 C 部分的代码来处理该问题

    在宏 CacheLookup 中,调用了宏(CacheHitCheckMissJumpMiss)用于处理方法缓存不同的命中情况:

    // 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 用于:

    1. 为执行下一个部分(慢速查找方法实现)的流程做准备
    2. 跳转到 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 函数主要负责:

    1. 在当前类对象中进行不加锁的方法缓存查找
    2. 加锁 && 检查当前类对象是否已知
    3. 如果当前类对象没有实现(isRealized)或者初始化(isInitialized),则实现或者初始化当前类对象
    4. 尝试在当前类对象的方法缓存以及方法列表中查找方法实现
    5. 尝试在父类对象的方法缓存以及方法列表中查找方法实现
    6. 如果没有找到方法实现,则尝试进行动态方法解析
    7. 如果动态方法解析失败,则尝试进行消息转发
    8. 解锁、返回方法实现
    // 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 使用汇编实现的原因:

    1. 所有 Objective-C 的方法调用最终都会被转换成消息发送,这导致 objc_msgSend 的调用频率非常的高,为了最大限度地提升代码性能,苹果采用了更贴近硬件底层的汇编语言来实现 objc_msgSend
    2. 在实际的开发过程中,方法调用者(即消息接受者)的返回值千奇百怪:有的返回一个对象类型的指针,有的返回一个整数,有的返回一个浮点数,…,有的什么也没有返回。为了适应花样繁多的返回值,在 void objc_msgSend(void /* id self, SEL op, ... */ ) 的函数声明中有这么一句注释:当使用此函数进行消息发送时,需要将此函数强制转换成适当的函数指针类型
      如果用 C、C++ 等高级语言来实现 objc_msgSend,则会出现这么一个问题:objc_msgSend 明明声明了一个 void 类型的返回值,但是却在代码实现中返回了一个非 void 类型的返回值,这在 C、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 进入消息转发阶段。这一点,也可以从方法名称看出来:

    1. lookUpImpOrNil:查找方法实现或者返回空
    2. 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 的方法缓存中:

    1. 如果在消息消息发送阶段(慢速查找方法实现)找到了 sel 所标识的方法,则会将相应的方法实现 imp 添加到当前类对象 cls 的方法缓存中
    2. 如果在消息转发阶段(动态方法解析、消息接受者重定向、完整的消息重定向)找到了 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);
    }
    
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值