阅读此文须要对于objc_object、objc_class以及结构体内部cache_t有必定的了解。c++
环境:xcode 11.5
算法
源码:objc4-781
数组
当咱们对oc
代码进行clang
处理转换为c++
代码以后,咱们会发现,oc
的方法调用会被进行以下转换:xcode
源码:
LGTeacher *teacher = [LGTeacher alloc];
[teacher sayHello];
转换以后:
LGTeacher *teacher = ((LGTeacher *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGTeacher"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)teacher, sel_registerName("sayHello"));
复制代码
最终都会调用消息发送objc_msgSend
方法,定义以下:缓存
objc_msgSend(id receiver, SEL op, ...)
第一个参数id receiver为消息的接收者
第二个参数SEL op为消息的名称SEL
...为可变参数
复制代码
objc_msgSend
是全部OC
方法调用的核心,调用效率是至关的高,所以处于性能考虑,这个函数的内部代码是用汇编来实现。在arm64
下的具体核心实现以下所示:markdown
以以下代码为例:
// objc_msgSend(id receiver, SEL op, ...)
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
//判断receiver是否为空
cmp p0, #0 // nil check and tagged pointer check
//处理异常逻辑
#if SUPPORT_TAGGED_POINTERS
//le less than or equal
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
// 读取x0寄存器即receiver的首地址所指向的内容存入p13,receiver是objc_object结构体指针,所以首地址对应的内存区域存放的是isa
ldr p13, [x0] // p13 = isa
// 从isa中获取Class,存入p16
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// 查找缓存,调用缓存或者进行快速查找objc_msgSend_uncached
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend
复制代码
objc_msgSend
的主要流程为:less
receiver
中查找到receiver
对应的isa
指针进而找到对应的类。接下来看一下具体的几个方法。函数
该方法主要用来从isa指针来获得对应的类。oop
.macro GetClassFromIsa_p16 /* src */
...
#elif __LP64__
//只关注arm64
// $0为上述流程中传入的参数p13,
// p16 = isa & #ISA_MASK, ISA_MASK为isa.h中声明的掩码,经过这一步操做,能够获得isa_t结构体中对应的cls
and p16, $0, #ISA_MASK
...
复制代码
.macro CacheLookup
//
// Restart protocol:
//
// As soon as we're past the LLookupStart$1 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$1,
// then our PC will be reset to LLookupRecover$1 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
//
LLookupStart$1:
//#define CACHE (2 * __SIZEOF_POINTER__)
// p1 = SEL, p16 = isa
// 读取x16+16个字节长度为地址的内容,存入p11中,根据objc_class的结构,cache_t和isa的偏移量就是16个字节,分别是isa8个字节以及superClassb8个字节
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
// 只关注arm64
// 0x0000ffffffffffff为arm64下的buckets掩码, 按位与以后获得buckets指针
and p10, p11, #0x0000ffffffffffff // p10 = buckets
// p1为msgSend的第二个参数_cmd
// p11=cache_t右移48位获得cache_t中的mask
// _cmd & mask和方法缓存中的哈希算法一致,以此来进行快速缓存方法查找
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
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
// PTRSHIFT 3
// p12即hash值左移4位至关于p12*16,获得对应的bucket指针相对于bucket数组的偏移量,进而获得当前hash值对应的bucket指针放入p12中
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// 读取x12内容为地址的值依次放入p17和p9中,arm64中bucket_t结构体布局为{imp,sel}
ldp p17, p9, [x12] // {imp, sel} = *bucket
// 比较p9=sel和p1=cmd
1: cmp p9, p1 // if (bucket->sel != _cmd)
// 不相等,向前跳转至2
b.ne 2f // scan more
// 相等,执行命中的指令
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
// 根据sel的值判断是否查找结束
CheckMiss $0 // miss if bucket->sel == 0
// 若是sel不为0,比较当前命中的bucket是否和buckets数组的第一个bucket相等
cmp p12, p10 // wrap if bucket == buckets
// 相等,跳转至3
b.eq 3f
// 不相等,以x12即当前bucket地址往前移动一个bucket大小为地址读取其中的值,并赋值为p17和p9,同时x12=x12-BUCKET_SIZE
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
// 返回1进行循环
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
// 接上步骤,若是p12和buckets数组第一个相等,须要移动到buckets数组的最后一位
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
// p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p12, p12, p11, LSL #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
// 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
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
// 若是从buckets数组最后一个开始往前寻找又找到了buckets数组第一个bucket,表明查找结束,并无找到当前的sel,执行JumpMiss操做。
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
JumpMiss $0
.endmacro
复制代码
这一步主要操做是在类objc_class中的缓存cache_t中快速查找是否有缓存。主要步骤以下:布局
_cmd&mask
获得hash
值hash
值获得对应的bucket->sel与_cmd
做对比CheckMiss
,若是bucket->sel==0
表明_cmd
对应的方法并无缓存过,或者在缓存扩容的时候被清空了。bucket->sel!=_cmd
,则往前一个bucket
继续进行对比,若是到达buckets数组
第一个则从数组的最后一个bucket
开始对比。bucket
开始往前查找又回到了第一个,则表明出现了问题,缓存已经满了,执行JumpMiss
。bucket->sel==_cmd
,命中缓存,执行CacheHit
。// CacheHit: x17 = cached IMP, x12 = address of cached IMP, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
// NORMAL状况下回执行调用imp的流程
TailCallCachedImp x17, x12, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
cbz p0, 9f // don't ptrauth a nil imp
AuthAndResignAsIMP x0, x12, x1, x16 // authenticate imp and re-sign as IMP
9: ret // return IMP
.elseif $0 == LOOKUP
// 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, x12, x1, x16 // authenticate imp and re-sign as IMP
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
复制代码
.macro CheckMiss
// miss if bucket->sel == 0
// cbz p9 判断p9是否为0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
// NORMAL状况下回执行调用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
// NORMAL状况下回执行调用objc_msgSend_uncached的流程
b __objc_msgSend_uncached
.elseif $0 == LOOKUP
b __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
复制代码
其中CheckMiss
和JumpMiss
都是快速查找方法没有找到的状况。
CheckMiss
是找到了一个空的位置,由于查找的算法和缓存的算法是一致的,意味着若是方法被缓存过,必然是放在当前的空位置。既然当前位置为空,说明该方法并无被缓存过。那么就要执行消息的慢速查找了。JumpMiss
表明整个缓存列表都找过了尚未找到_cmd
对应的方法,这种状况我有些疑问,cache_t是会扩容的,是否是意味着在正常的状况下必定会有空位,那为何还能遇到列表满的状况?有大佬的话能够解答一下。以上是objc_msgSend发送消息时在汇编层面的快速查找流程,进入__objc_msgLookup_uncached
就属于慢速查找了,后面再议。
快速查找主要流程图以下: