一、前言
众所周知,OC 是一门动态语言,因为 runtime 的存在而变得强大,而在代码中调用方法就是给对象发送消息也是因为 runtime 的存在,调用方法就是调用 objc_msgSend 这个函数,那在底层又是怎么样的呢?汇编又是怎么一步步调用的呢?这篇文章会将通过汇编来分析 objc_msgSend 都做了啥。
二、OC 方法底层是什么样的
1、将方法进行 clang 编译
我们在 main 中写两个方法,然后对其进行 clang 一下,在 .cpp 文件最后能发现如下代码。

objc_msgSend 函数,第一个参数是消息接受者,第二个参数是方法名称(第二个参数可以替换成我们很熟悉的 @selector)。
简单来说给
OC对象发送消息就是找函数实现的过程,OC方法底层就是通过sel去找imp的过程,而C函数名就是函数指针,通过函数指针就可以直接找到函数实现。
三、通过汇编分析 objc_msgSend
1、如何去寻找 objc_msgSend 源码
在 main 函数中给方法打断点进行汇编分析,然后跳到 objc_msgSend 函数里面,如右图,就能得知 objc_msgSend 源码需要去 libobjc.A.dylib 库中找,操作如下图。

疑问点:为什么
objc_msgSend是一段汇编,而不是C或者是C++更加直接呢?个人观点:
1.汇编更加容易被机器识别 2.参数未知,对于静态的
C或者是C++来说是很难接受的
2、 objc_msgSend 汇编分析
我们来到 objc.750版本 源码中,通过全局搜索 objc_msgSend,找到在 objc-msg-arm64.s 的汇编代码。
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, // 判断当前 p0 寄存器是否为空,当前 p0 存的是 objc_object 对象地址
// 处理对象是 tagged pointer 或 nil 的情况
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged
#else
b.eq LReturnZero
#endif
// 为正常的消息发送流程,就会走如下代码
ldr p13, [x0] // p13 = isa,把 x0 指向内存的前 64 位放到 p13(即是 objc_object 的 isa 成员变量)
GetClassFromIsa_p16 p13 // p16 = class,是一个宏,取面具,isa & ISA_MASK,得到当前类
LGetIsaDone:
CacheLookup NORMAL // 查找缓存
此时对 isa 处理已经完成,已经找到当前类,接下来就是去缓存里面找方法,如果有直接返回对应的 imp ,接下来我们通过 command + F 搜索 CacheLookup,发现 CacheLookup 的参数分为三种,NORMAL(正常的去查找) 、 GETIMP(直接返回 IMP) 和 LOOKUP(主动的慢速去查找)。
.macro CacheLookup
// p1 = SEL, p16 = isa
// x16代表 class,#CACHE 是一个宏定义 #define CACHE (2 * __SIZEOF_POINTER__),代表16个字节
// class 平移 CACHE(也就是16个字节)得到 cache_t,然后将 cache_t里面的 buckets 和 occupied|mask 赋值给 p10和p11
// 为什么 occupied|mask 两个值给了一个寄存器呢?因为 occupied|mask 都是只占4字节,而一个寄存器是8字节,这样赋值给一个寄存器节省内存
ldp p10, p11, [x16, #CACHE] // p10 = buckets, p11 = occupied|mask
#if !__LP64__
and w11, w11, 0xffff // p11 = mask,iOS 为小端模式,w11只取前面四个字节,为 mask
#endif
and w12, w1, w11 // x12 = _cmd & mask,得到当前方法 hash 表的下标
add p12, p10, p12, LSL #(1+PTRSHIFT) // LSL 左移
// p10(buckets) 平移 p12 左移 #(1+PTRSHIFT) 之后的值
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
ldp p17, p9, [x12] // {imp, sel} = *bucket,通过 bucket 取出方法的 imp 和 sel
// 判断 bucket 的 sel 和 _cmd 是否相同,p9为sel
1: cmp p9, p1 // if (bucket->sel != _cmd)
// 如果不同,走第二步,也就是 CheckMiss
b.ne 2f // scan more
// 如果相同,就会命中缓存,直接返回 imp,当前的 imp 存在 $0 里面
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
// 比较 bucket
cmp p12, p10 // wrap if bucket == buckets
// 相同,则会走第三步,将上面流程再走一次,重新查找一次,如果还是查找不到就会 JumpMiss
b.eq 3f
// 不同,就会 *--bucket 循环
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
add p12, p12, w11, UXTW #(1+PTRSHIFT) // p12 = buckets + (mask << 1+PTRSHIFT)
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp p17, p9, [x12] // {imp, sel} = *bucket
上述分析,感觉 CheckMiss 里面应该有我们想找的代码,接下来就去分析一下 CheckMiss。
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
当前参数为 NORMAL,所以如果没找到就会走 __objc_msgSend_uncached。
3、 CacheLookup 小结
根据上述分析得出 CacheLookup 包含读取方法缓存的核心逻辑,主要产生两种结果:若缓存命中,返回 IMP 或调用 IMP;若缓存未命中,调用 __objc_msgSend_uncached (找到IMP会调用) 或 __objc_msgLookup_uncached (找到IMP不会调用) 方法。
四、 MethodTableLookup 分析
当 CheckMiss 来到 __objc_msgSend_uncached
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
MethodTableLookup 后面是比较复杂的逻辑,下面会分析,TailCallFunctionPointer x17 若找到了 IMP 会放到 x17 寄存器中,然后把 x17 的值传递给 TailCallFunctionPointer 宏调用方法。
MethodTableLookup
.macro MethodTableLookup
// push frame
SignLR
// 后面要跳转函数,意味着lr的变化,所以开辟栈空间后需要把之前的fp/lr值存储到栈上便于复位状态
stp fp, lr, [sp, #-16]!
mov fp, sp
// save parameter registers: 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)]
// receiver and selector already in x0 and x1
mov x2, x16
// bl 是跳转,跳转到 __class_lookupMethodAndLoadCache3 方法
bl __class_lookupMethodAndLoadCache3
// IMP in x0
mov x17, x0
// restore registers and return
...
mov sp, fp
ldp fp, lr, [sp], #16
AuthenticateLR
.endmacro
当我们进行全局搜索 __class_lookupMethodAndLoadCache3 方法的时候却怎么也搜索不到,__ 代表着汇编函数,而 __class_lookupMethodAndLoadCache3 是 C 函数,我们去掉一个 _ 进行全局搜索,我们就来到了 lookUpImpOrForward 消息查找流程。
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
五、总结
- <1>
ENTRY _objc_msgSend - <2> 对消息接受者
(id self,sel _cmd)判断处理 - <3>
LNilOrTagged判断处理 - <4>
GetClassFromIsa_p16的isa的指针处理,isa & ISA_MASK得到当前的类 - <5>
CacheLookup查找缓存 - <6>
cache_t处理bucket以及内存哈希的处理 - <7>
__objc_msgSend_uncached告诉找不到缓存的imp - <8>
MethodTableLookup跳转到方法查找的流程 - <9>
bl __class_lookupMethodAndLoadCache3方法查找的流程开始