前文提要
iOS 底层探索文章系列
我们在前面探索了对象和类的底层原理,接下来我们要探索一下方法的本质,而在探索之前,我们先简单了解 Runtime 的知识点。
1、Runtime 简介
1.1 Runtime 版本
首先 Runtime 分为两个版本,legacy 和 modern,分别对应 Objective-C 1.0 和 Objective-C 2.0。我们通常只需要专注于 modern 版本即可。
1.2 Runtime 三种交互方式
我们与 Runtime 的交互有以下三种方式
- 直接在
OC层进行交互:比如@selector; NSObjCRuntime的:NSSelectorFromString方法;Runtime API:sel_registerName。
2、探索方法的本质
2.1 clang 重写
我们先将下面这段代码进行 clang 重写

int main(int argc, const char * argv[]) {/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;ZLStudent *student = ((ZLStudent *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ZLStudent"), sel_registerName("alloc"));((void (*)(id, SEL))(void *)objc_msgSend)((id)student, sel_registerName("sayHello"));}return 0;}
我们可以看到,经过重写之后,sayHello 方法在底层其实就是一个消息的发送。我们把上面的发送消息的代码简化一下:
ZLStudent *student = objc_msgSend((id)objc_getClass("ZLStudent"), sel_registerName("alloc"));objc_msgSend((id)student, sel_registerName("sayHello"));
如果要在工程当中直接使用 objc_msgSend API,我们需要导入头文件 <objc/message.h> 和 将 Enbale Strict Checking of objc_msgSend Calls 设置为 NO,这样才不会报错。

接下来我们打印一下结果,

可以看到,也是可以正常调用方法的,由此可见,真正发送消息的地方是 objc_msgSend,这个方法有基本的两个参数,第一个参数是消息的接收者为 id 类型,第二个参数是方法编号为 SEL 类型。
2.2 发送方法的几种形式

上诉代码表示的是向对象 student 发送 sayHello 消息

上述代码表示向 ZLStudent 这个类发送 sayHappy 消息。

上述代码表示向父类 ZLTeacher 发送 sayGoodMorning 消息。

上述代码表示向父类的类,也就是 ZLTeacher 类对象的元类发送 sayByebye 消息。
3、探索 objc_msgSend(重点)
objc_msgSend 之所以采用汇编来实现,有以下两种主要因素:
- 汇编更容易能被机器识别
- 参数未知、类型未知对于
C和C++来说不如汇编更得心应手
3.1 消息查找机制
消息查找机制分为两个机制
- 快速流程
- 慢速流程
接下来我们开始本文的重点,针对 方法缓存 cache_t 的分析(本文采用的源码版本为 objc4-781 )
3.2 cache_t 源码分析
我们知道,当我们的 OC 项目在编译完成之后,类的实例方法(方法编号 SEL 和函数指针地址 IMP)会保存在类的方法列表中。
OC 为了实现其动态性,将 方法的调用包装成了 SEL 寻找 IMP 的过程。我们可以想象一下,如果每次调用方法,都要去类的方法列表或者父类、根类的方法列表里面去查询函数地址的话,必然会对性能造成极大的损耗。为了解决这个问题,OC 采用了方法缓存的机制来提高调用效率,也就是 cache_t,其作用就是缓存已调用的方法。当调用方法时,objc_msgSend 会先去缓存中查找,如果找到就执行该方法;如果不在缓存中,则去类的方法列表或者父类、根类的方法列表去查找,找到后会将方法的 SEL 和 IMP 缓存到 cache_t 中,以便下次调用时能够快速执行。
3.3 objc_msgSend 读取缓存
之前我们已经分析过 cache_t 写入缓存的工作流程,下面我们来分析一下 objc_msgSend 读缓存的代码。( 以 arm64 架构汇编为例 )
3.3.1 _objc_msgSend 入口函数源码实现:
ENTRY _objc_msgSendUNWIND _objc_msgSend, NoFrame// 判断P0,也就是我们 `objc_msgSend` 的第一个参数 `id` 消息的接收者是否存在cmp p0, #0 // nil check and tagged pointer check// 是否是 `taggedPointer` 对象判断处理#if SUPPORT_TAGGED_POINTERS// `tagged` 或者空判断b.le LNilOrTagged // (MSB tagged pointer looks negative)#else// 直接返回空b.eq LReturnZero#endif// 读取 `x0`,然后复制到 `p13`,这里 `p13` 拿到的是 `isa`。为什么要拿 `isa` 呢,因为不论是对象方法还是类方法,我们都需要在类或者元类的缓存或者方法列表中去查找,所以 `isa` 是必须的。ldr p13, [x0] // p13 = isa// 通过 `GetClassFromIsa_p16`,将获取到的 `class` 存到 `p16`。GetClassFromIsa_p16 p13 // p16 = classLGetIsaDone:// calls imp or objc_msgSend_uncached// 获取完 `isa` 之后,接下来就要进行 `CacheLookup`,进行查找方法缓存,我们接下来到 `CacheLookup`的源码处CacheLookup NORMAL, _objc_msgSend
通过注释,我们知道 CacheLookup 有三种模式
NORMALGETIMPLOOKUP
3.3.2 CacheLookup 源码实现:
// CacheLookup NORMAL|GETIMP|LOOKUP <function>.macro CacheLookup// p1 = SEL, p16 = isa// `CacheLookup` 需要读取上一步拿到的类的 `cache` 缓存,然后进行 16 字节地址平移操作,把 `cache_t` 中的 `_maskAndBuckets` 复制给 `p11`。ldr p11, [x16, #CACHE] // p11 = mask|buckets// 将 `_maskAndBuckets` & `bucketsMask` 掩码,然后将结果放在 `p10 = buckets`and p10, p11, #0x0000ffffffffffff // p10 = buckets// LSP逻辑右移,将 `p11` 右移48位得到 `mask`, `sel & mask` 后把结果放入到 `p12`,这里的本质就是我们在写入内存遇到的 `cache_hash` 方法一模一样,目的就是拿到方法缓存的哈希下标。and p12, p1, p11, LSR #48 // x12 = _cmd & mask// LSL逻辑左移,`p10` 是 `buckets` 也是缓存数组的首地址,每个 `bucket(sel 8字节 + imp 8字节)` 的大小为 16 字节,`p12` 为方法缓存的哈希下标,`buckets + (index << 4)` 得到 下标处对应的 `bucket`,然后把结果放到 `p12`。add p12, p10, p12, LSL #(1+PTRSHIFT)// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))// 从 `bucket` 结构体将中 `imp` 和 `sel` 分别存到 `p17` 和 `p9` 中。ldp p17, p9, [x12] // {imp, sel} = *bucket// 接着我们将上一步获取到的 `sel` 和我们要查找的 `sel`(在这里也就是所谓的 `_cmd`)进行比较,如果匹配了,就通过 `CacheHit` 将 imp 返回;如果没有匹配,就走下一步流程。1: cmp p9, p1 // if (bucket->sel != _cmd)b.ne 2f // scan more// 命中缓存,返回结果CacheHit $0 // call or return imp// 没找到 `bucket`2: // not hit: p12 = not-hit bucket// 如果从最后一个元素往前遍历都找不到缓存,那么走 `CheckMiss`CheckMiss $0 // miss if bucket->sel == 0// 判断当前查询的 `bucket` 是否为第一个元素cmp p12, p10 // wrap if bucket == buckets// 如果是第一个元素,那么将当前查询的` bucket` 设置为最后一个元素 `(p12 = buckets + (mask << 1+PTRSHIFT))`b.eq 3f// 向前查找ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket// 进行递归搜索b 1b // loop3: // wrap: p12 = first bucket, w11 = mask#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))// p12 = buckets + (mask << 1+PTRSHIFT)#endifldp p17, p9, [x12] // {imp, sel} = *bucket1: cmp p9, p1 // if (bucket->sel != _cmd)b.ne 2f // scan moreCacheHit $0 // call or return imp2: // not hit: p12 = not-hit bucketCheckMiss $0 // miss if bucket->sel == 0cmp p12, p10 // wrap if bucket == bucketsb.eq 3fldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket// 递归遍历b 1b // loopLLookupEnd$1:LLookupRecover$1:3: // double wrapJumpMiss $0
3.3.3 CheckMiss 源码实现:
.macro CheckMiss// miss if bucket->sel == 0.if $0 == GETIMPcbz p9, LGetImpMiss.elseif $0 == NORMAL// 由于我们是 `NORMAL` 模式,所以会来到这 `__objc_msgSend_uncached`cbz p9, __objc_msgSend_uncached.elseif $0 == LOOKUPcbz p9, __objc_msgLookup_uncached.else.abort oops.endif.endmacro
3.3.4 __objc_msgSend_uncached 源码实现:
STATIC_ENTRY __objc_msgSend_uncachedUNWIND __objc_msgSend_uncached, FrameWithNoSaves// THIS IS NOT A CALLABLE C FUNCTION// Out-of-band p16 is the class to search// 这里面最核心的逻辑就是 `MethodTableLookup`,查找方法列表。MethodTableLookupTailCallFunctionPointer x17END_ENTRY __objc_msgSend_uncached
3.3.5 MethodTableLookup 源码实现:
.macro MethodTableLookup// push frameSignLRstp fp, lr, [sp, #-16]!mov fp, sp// save parameter registers: x0..x8, q0..q7sub 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)]// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)// receiver and selector already in x0 and x1mov x2, x16mov x3, #3// `重点`bl _lookUpImpOrForward// IMP in x0mov x17, x0// restore registers and returnldp 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, fpldp fp, lr, [sp], #16AuthenticateLR.endmacro
我们观察 MethodTableLookup 内容之后会定位到 _lookUpImpOrForward。真正的方法查找流程核心逻辑是位于 _lookUpImpOrForward 里面的。 但是我们全局搜索 _lookUpImpOrForward 会发现找不到,这是因为此时我们会从 汇编 跳入到 C/C++。所以去掉一个下划线就能找到了

3.4 objc_msgSend 流程图

4、总结
- 方法的本质就是消息发送,消息发送是通过
objc_msgSend以及其派生函数来实现的。 objc_msgSend为了执行效率以及C/C++不能支持参数未知,类型未知的代码,所以采用 汇编 来实现objc_msgSend。- 消息查找或者说方法查找,会优先去从类中查找缓存,找到了就返回,找不到就需要去 类的方法列表 中查找。
- 由汇编过渡到
C/C++,在类的方法列表中查找失败之后,会进行转发。核心逻辑位于lookUpImpOrForward。
