本文的主要目的是理解objc_msgSend
的方法查找流程
,首先查找的是cache
缓存而且用汇编
实现的,因此称之为快速查找,对应的methoList查询,称之为慢速查找。
之前的流程分析了cache insert buckets
的流程,以及LLDB
调试获取buckets
的过程,那么objc_msgSend
查找cache流程
与我们手动LLDB查找
非常类似而且原理是一样一样的
1.Runtime介绍
runtime称为运行时,它区别于编译时
运行时
是代码跑起来,被装载到内存中
的过程,如果此时出错,则程序会崩溃,是一个动态
阶段
编译时
是源代码翻译成机器能识别的代码
的过程,主要是对语言进行最基本的检查报错,即词法分析、语法分析等,是一个静态
的阶段
runtime
的使用有以下三种方式,其三种实现方法与编译层和底层的关系如图所示
通过OC代码
,例如 [person sayNB]
通过NSObject方法
,例如isKindOfClass
通过Runtime API
,例如class_getInstanceSize
2.方法的本质
使用clang编译main.cpp文件,通过查看main函数中方法调用的实现,如下所示
CJLPerson * person = [CJLPerson alloc];
[person sayHello];
CJLPerson * person = ((CJLPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("CJLPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
通过上述代码可以看出,方法的本质就是objc_msgSend
消息发送
1.objc_msgSend汇编查询 arm64架构下源码入口
2.objc4-818.2 objc_msgSend 查询cache流程图
3.objc_msgSend 汇编查询cache源码
1.ENTRY _objc_msgSend,入口,获取isa和class
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check // 判断receiver是否为空
#if SUPPORT_TAGGED_POINTERS // 支持小对象
b.le LNilOrTagged // (MSB tagged pointer looks negative) // 支持小对象调整
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa //x0寄存器获取isa
GetClassFromIsa_p16 p13, 1, x0 // p16 = class // 根据isa获取class
LGetIsaDone: // 获取isa后面的操作流程
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached // 跳转到CacheLookup代码段
//CacheLookup调用完毕,如果没有CacheHit,则执行这个代码段 __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
主要有以下几步
第一,判断objc_msgSend
方法的第一个参数receiver
是否为空
第二,是否支撑小对象,如果支持tagged pointer
,跳转至LNilOrTagged ---> GetTaggedClass ---> LGetIsaDone
如果小对象为空,则直接返回空,即LReturnZero
第三,获取isa,p13 = isa
, 获取class,GetClassFromIsa_p16
,通过 isa & ISA_MASK
获取shiftcls
位域的类信息,即class,p16 = class
第四,执行标签LGetIsaDone: ---> CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
,进入CacheLookup
代码段
2 .macro CacheLookup 代码段
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
//
// Restart protocol:
//
// As soon as we're past the LLookupStart\Function label we may have
// loaded an invalid cache pointer or mask.
//
// When task_restartable_ranges_synchronize() is called,
// (or when a signal hits us) before we're past LLookupEnd\Function,
// then our PC will be reset to LLookupRecover\Function which forcefully
// jumps to the cache-miss codepath which have the following
// requirements:
//
// GETIMP:
// The cache-miss is just returning NULL (setting x0 to 0)
//
// NORMAL and LOOKUP:
// - x0 contains the receiver
// - x1 contains the selector
// - x16 contains the isa
// - other registers are set as per calling conventions
//
mov x15, x16 // stash the original isa
LLookupStart\Function:
// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
ldr p10, [x16, #CACHE] // p10 = mask|buckets
lsr p11, p10, #48 // p11 = mask
and p10, p10, #0xffffffffffff // p10 = buckets
and w12, w1, w11 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16 // 64位真机
ldr p11, [x16, #CACHE] // p11 = mask|buckets // x16 平移16字节到cache,16 = isa 8 + superclass 8
#if CONFIG_USE_PREOPT_CACHES // p11 = _bucketsAndMaybeMask,即cache的第一个8字节
#if __has_feature(ptrauth_calls)// #define CLASS __SIZEOF_POINTER__ #define CACHE (2 * __SIZEOF_POINTER__) --> 2 * 8 = 16
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
and p10, p11, #0x0000fffffffffffe // p10 = buckets
tbnz p11, #0, LLookupPreopt\Function
#endif
eor p12, p1, p1, LSR #7
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else // _bucketsAndMaybeMask = mask(高位16) + buckets指针(低48位) p10 = buckets
and p10, p11, #0x0000ffffffffffff // p10 = buckets // _bucketsAndMaybeMask & 0x0000ffffffffffff(后48位为1)
and p12, p1, p11, LSR #48 // x12 = _cmd & mask // p1 = _cmd,_bucketsAndMaybeMask逻辑右移48位获取到mask
#endif // CONFIG_USE_PREOPT_CACHES // p12 = _cmd & mask = 初始哈希下标(begin)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
ldr p11, [x16, #CACHE] // p11 = mask|buckets
and p10, p11, #~0xf // p10 = buckets
and p11, p11, #0xf // p11 = maskShift
mov p12, #0xffff
lsr p11, p12, p11 // p11 = mask = 0xffff >> p11
and p12, p1, p11 // x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif
// #define PTRSHIFT 3
add p13, p10, p12, LSL #(1+PTRSHIFT) // p12 逻辑左移4位即扩大16倍,指针平移到对应的bucket位置上
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) // p13 指向哈希下标对应的bucket
// insert bucket的时候,do-while写入,哈希和二次哈希,读取的时候也是do-while读取cache
// do { // p17 = imp, p9 = sel,bucket中imp和sel分别赋给p17和p9
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket-- //赋值完成后p13 -= BUCKET_SIZE,指向前一个bucket
cmp p9, p1 // if (sel != _cmd) { //获取的sel和_cmd,如果不相等,调整到3f,
b.ne 3f // scan more
// } else {
2: CacheHit \Mode // hit: call or return imp //获取的sel和_cmd,如果相等,缓存命中,call or return imp
// }
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss; // 如果取出的sel位nil,则goto Miss
cmp p13, p10 // } while (bucket >= buckets) //如果bucket >= buckets,即没有到最前面
b.hs 1b // 则,继续比较前一个bucket,如果到最前面继续执行后续代码
// wrap-around:
// p10 = first bucket
// p11 = mask (and maybe other bits on LP64)
// p12 = _cmd & mask
//
// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
// So stop when we circle back to the first probed bucket
// rather than when hitting the first bucket again.
//
// Note that we might probe the initial bucket twice
// when the first probed slot is the last entry.
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
add p13, p10, w11, UXTW #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT)) //p11 = _bucketsAndMaybeMask,逻辑右移44位,相当于mask逻辑左移4位
// p13 = buckets + (mask << 1+PTRSHIFT) //指向最后的bucket
// see comment about maskZeroBits //正常是p11右移48位获取到mask
// 再左移4位获取到mask指向的bucket,相当于p11右移了44位
// bucket >= buckets,再次从最后到最前面进行一次do-while循环查找
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p13, p10, p11, LSL #(1+PTRSHIFT)
// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = first probed bucket
// do {
4: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket-- // 这里重复1:标签,从mask--->0 查找,从后到前查找
cmp p9, p1 // if (sel == _cmd) // 汇编里没有do-while,p13指向最后再重复一次1:标签的逻辑
b.eq 2b // goto hit
cmp p9, #0 // } while (sel != 0 &&
ccmp p13, p12, #0, ne // bucket > first_probed)
b.hi 4b
LLookupEnd\Function:
LLookupRecover\Function:
b \MissLabelDynamic
主要分为以下几步
p1 = SEL , p16 = isa
第一,获取到指向cache
和_bucketsAndMaybeMask
通过p16 = class = isa
,首地址平移16
字节(因为在objc_class中,首地址距离cache
正好16
字节,即isa
首地址 占8
字节,superClass
占8
字节),获取cahce,p11
指向cache
中第一个8
字节_bucketsAndMaybeMask
, _bucketsAndMaybeMask
中高16
位存mask
,低48
位存buckets
, _bucketsAndMaybeMask = mask(高位16) + buckets指针(低48位)
,即p11 = _bucketsAndMaybeMask
第二,从_bucketsAndMaybeMask
中分别取出buckets和mask
,并由mask
根据哈希算法计算出哈希下标
p10
= _bucketsAndMaybeMask & 0x0000ffffffffffff
= buckets
,
_bucketsAndMaybeMask >> 48
= mask
p12
= _cmd & mask
= 哈希下标
,记作 begin
将objc_msgSend的参数p1(即第二个参数_cmd)& msak,通过哈希算法,得到需要查找存储sel-imp的bucket下标begin,即p12 = begin = _cmd & mask,为什么通过这种方式呢?因为在存储sel-imp时,也是通过同样哈希算法计算哈希下标进行存储
第三,根据所得的哈希下标begin
和 buckets
首地址,取出哈希下标对应的bucket
add p13, p10, p12, LSL #(1+PTRSHIFT)
#define PTRSHIFT 3
p12 = begin
逻辑左移4位
,即扩大16
倍,一个bucket
占用16
个字节,即sizeof(bucket_t) = 16
,sel
占用8
字节,imp
占用8
字节,p12左移4位
就是按照结构体bucket_t
的步长
在移动指针,和alloc
16字节对齐算法原理一样
newX = (x + 15) >> 4
,16以下清零,缩小16倍
newX << 4
,扩大16倍
,恢复
p10 = buckets,首地址,first bucket
,
p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
p13 = buckets + begin * 16
---> 指向begin
哈希下标对应的bucket
根据计算的哈希下标begin
乘以 单个bucket
占用的内存大小,得到buckets首地址
距离begin
下标指向的bucket
在实际内存中的偏移量
通过首地址
+ 实际偏移量
,获取哈希下标begin对应的bucket
第四,进入do-while
循环步骤如下
第一步,取出sel
和imp
ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
首先取出p13
指向的当前bucket
里面的imp
和sel
,p17 = imp,p9 = sel
,赋值完成后p13 -= BUCKET_SIZE
,指向前一个bucket
第二步,p9和_cmd
是否相等
cmp p9, p1
p9 == p1
,缓存命中执行CacheHit
不相等,执行下面的逻辑
第三步,p9 == nil ? p9是否为nil
cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss
,
如果p9 == nil
,则指向goto Miss
,默认没找到,这里忽略了哈希冲突后二次哈希可能导致begin
下标和真实写入的index
之间存在差异, 而且初始化或扩容
后,里面的bucket
都是空的sel和imp
都是``nil,直接简单粗暴,
p9即指向的sel为
nil```,则认为丢失,也是为了更快
第四步,p9 != nil
,判断p13
是否 已经执行到最前面了
cmp p13, p10 // } while (bucket >= buckets)
如果bucket >= buckets
,则跳转到第一步,while
循环开始,while (bucket < buckets)
while循环结束
,依然没有找到,则跳转到最后的bucket
,即mask
下标所指向的bucket
,从后到前
再次查找一遍
第五步,begin --> 0
,依然没有找到,跳转到最后,mask指向的bucket
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
// p13 = buckets + (mask << 1+PTRSHIFT)
//p11 = _bucketsAndMaybeMask
,逻辑右移44
位,相当于mask逻辑左移4位
// p13 = buckets + (mask << 1+PTRSHIFT)
//指向最后的bucket
正常是p11右移48位
获取到mask
,再左移4
位,相当于_bucketsAndMaybeMask右移44位
此时p13
,指向最后的bucket,while循环
,跳转到第一步
以上流程总结
第一次do-while循环
,从begin ---> 0
查找一遍,如果没命中,p9不为nil,开始第二次do-while循环
第二次do-while循环
,从mask ---> 0
再次查找一遍,
依然如此,则执行__objc_msgSend_uncached ---> MethodTableLookup ---> _lookUpImpOrForward
开始查找方法列表
CacheHit
// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp //调用imp
.elseif $0 == GETIMP
mov p0, p17
cbz p0, 9f // don't ptrauth a nil imp
AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP
9: ret // return IMP //返回imp
.elseif $0 == LOOKUP // 执行__objc_msgSend_uncached,开始方法列表查找
// No nil check for ptrauth: the caller would crash anyway when they
// jump to a nil IMP. We don't care if that jump also fails ptrauth.
AuthAndResignAsIMP x17, x10, x1, x16 // authenticate imp and re-sign as IMP
cmp x16, x15
cinc x16, x16, ne // x16 += 1 when x15 != x16 (for instrumentation ; fallback to the parent class)
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
__objc_msgSend_uncached
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p15 is the class to search
MethodTableLookup //核心代码段
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
MethodTableLookup
.macro MethodTableLookup
SAVE_REGS MSGSEND
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward //bl跳转,cache里找不到,跳转到方法列表里查找
// IMP in x0
mov x17, x0
RESTORE_REGS MSGSEND
.endmacro
4.总结
isa ---> class ---> cache ---> _bucketsAndMaybeMask ---> mask 和 buckets ---> (buckets + mask << 4) == current bucket
1.current bucket ---> imp和sel ---> current bucket -= BUCKET_SIZE,指向前一个bucket
2.sel == _cmd,缓存命中,CacheHit ---> hit: call or return imp,cache查找流程结束
3.sel != _cmd,sel == nil,goto Miss,cache查找流程结束,执行6
4.sel != nil 且bucket >= buckets,即没到最前面,则执行 begin ---> 0执行的bucket,do-while循环检查
5.bucket < buckets,则bucket指向最后,buckets + (mask << 4),bucket >= buckets,do-while循环检查,执行1
6.两遍依然没找到imp,则执行__objc_msgSend_uncached ---> MethodTableLookup ---> _lookUpImpOrForward开始查找方法列表
7. 汇编里没有do-while,p13指向最后再重复一次1:标签的逻辑