objc_msgSend分析-快速查找

objc_msgSend快速查找

阅读此文须要对于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

  1. 从当前的消息接收者receiver中查找到receiver对应的isa指针进而找到对应的类。
  2. 在类中查找对应的方法并执行

接下来看一下具体的几个方法。函数

GetClassFromIsa_p16

该方法主要用来从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
...
复制代码

CacheLookup

.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中快速查找是否有缓存。主要步骤以下:布局

  1. 经过_cmd&mask获得hash
  2. 根据hash值获得对应的bucket->sel与_cmd做对比
  3. 执行CheckMiss,若是bucket->sel==0表明_cmd对应的方法并无缓存过,或者在缓存扩容的时候被清空了。
  4. 若是bucket->sel!=_cmd,则往前一个bucket继续进行对比,若是到达buckets数组第一个则从数组的最后一个bucket开始对比。
  5. 若是从最后一个bucket开始往前查找又回到了第一个,则表明出现了问题,缓存已经满了,执行JumpMiss
  6. 若是bucket->sel==_cmd,命中缓存,执行CacheHit

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


复制代码

CheckMiss

.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
复制代码

JumpMiss

.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
复制代码

其中CheckMissJumpMiss都是快速查找方法没有找到的状况。

  • CheckMiss是找到了一个空的位置,由于查找的算法和缓存的算法是一致的,意味着若是方法被缓存过,必然是放在当前的空位置。既然当前位置为空,说明该方法并无被缓存过。那么就要执行消息的慢速查找了。
  • JumpMiss表明整个缓存列表都找过了尚未找到_cmd对应的方法,这种状况我有些疑问,cache_t是会扩容的,是否是意味着在正常的状况下必定会有空位,那为何还能遇到列表满的状况?有大佬的话能够解答一下。

以上是objc_msgSend发送消息时在汇编层面的快速查找流程,进入__objc_msgLookup_uncached就属于慢速查找了,后面再议。

快速查找主要流程图以下:

相关文章
相关标签/搜索