众所周知,OC 是一门动态语言,由于 runtime
的存在而变得强大,而在代码中调用方法就是给对象发送消息也是由于 runtime
的存在,调用方法就是调用 objc_msgSend
这个函数,那在底层又是怎么样的呢?汇编又是怎么一步步调用的呢?这篇文章会将经过汇编来分析 objc_msgSend
都作了啥。缓存
clang
编译咱们在 main
中写两个方法,而后对其进行 clang
一下,在 .cpp
文件最后能发现以下代码。bash
objc_msgSend
函数,第一个参数是消息接受者,第二个参数是方法名称(第二个参数能够替换成咱们很熟悉的
@selector
)。
简单来讲给
OC
对象发送消息就是找函数实现的过程,OC
方法底层就是经过sel
去找imp
的过程,而C
函数名就是函数指针,经过函数指针就能够直接找到函数实现。函数
objc_msgSend
objc_msgSend
源码在 main
函数中给方法打断点进行汇编分析,而后跳到 objc_msgSend
函数里面,如右图,就能得知 objc_msgSend
源码须要去 libobjc.A.dylib
库中找,操做以下图。oop
疑问点:为何
objc_msgSend
是一段汇编,而不是C
或者是C++
更加直接呢?post我的观点:ui
1.汇编更加容易被机器识别 2.参数未知,对于静态的
C
或者是C++
来讲是很难接受的spa
objc_msgSend
汇编分析咱们来到 objc.750版本
源码中,经过全局搜索 objc_msgSend
,找到在 objc-msg-arm64.s
的汇编代码。3d
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
。rest
.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
。
根据上述分析得出 CacheLookup
包含读取方法缓存的核心逻辑,主要产生两种结果:若缓存命中,返回 IMP
或调用 IMP
;若缓存未命中,调用 __objc_msgSend_uncached
(找到IMP会调用) 或 __objc_msgLookup_uncached
(找到IMP不会调用) 方法。
当 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*/);
}
复制代码
ENTRY _objc_msgSend
(id self,sel _cmd)
判断处理LNilOrTagged
判断处理GetClassFromIsa_p16
的 isa
的指针处理,isa & ISA_MASK
获得当前的类CacheLookup
查找缓存cache_t
处理 bucket
以及内存哈希的处理__objc_msgSend_uncached
告诉找不到缓存的 imp
MethodTableLookup
跳转到方法查找的流程bl __class_lookupMethodAndLoadCache3
方法查找的流程开始