OC底层原理objc_msgSend之方法的快速查找流程

3,255 阅读7分钟

前言

前文OC底层原理之类的结构分析分析了cache_t的基本结构,那么缓存是在什么时候读取和插入的呢?本文将对缓存的读取进行探索。

通过前文的分析,我们已经知道,cache_t是对方法的缓存,那么缓存插入,读取必然和发送消息有关。接下来我们分别对调用方法和performSelector进行探索:

一、方法调用分析

1.方法调用: 接下来,如下图所示,创建一个XQPerson的对象,并调用eatFood方法:

image.png

我们通过clang -rewrite-objc main.m -o main.cpp编译成cpp文件:

image.png

可以看到,调用方法实际上是调用objc_msgSend函数发送消息

2.performSelector 查看performSelector方法的源码如下: image.png

我们可以看到,performSelector实际上也是调用objc_msgSend函数发送消息。

小结:方法调用的实质都是调用objc_msgSend函数发送消息。

接下来,我们就可以把探索的方向锁定在objc_msgSend

二、objc_msgSend汇编分析(本文仅针对arm64真机环境)

准备工作:

下载源码objc-818.2

全局搜索objc_msgSend,按住command键点击搜索结果左边的小箭头收起搜索结果,选中objc-msg-arm64.s文件,找到ENTRY _objc_msgSend,代码如下所示:

ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame
        //将p0(消息接收者)与0进行比较
	cmp	p0, #0			// nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
        // 判断tagged pointer b.le :判断上面cmp的值是小于等于 执行LNilOrTagged,否则直接往下走
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
        //判断是否为空  b.eq :cmp结果 等于 0 执行地址LReturnZero 否则往下
	b.eq	LReturnZero
#endif
        //将[x0]数据写入到寄存器中 即:p13寄存器保存了 对象的isa
	ldr	p13, [x0]		// p13 = isa
        //得到calss
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class
LGetIsaDone:
        //开始查找缓存
	// calls imp or objc_msgSend_uncached
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
	b.eq	LReturnZero		// nil check
	GetTaggedClass
	b	LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif

LReturnZero:
	// x0 is already zero
	mov	x1, #0
	movi	d0, #0
	movi	d1, #0
	movi	d2, #0
	movi	d3, #0
	ret

	END_ENTRY _objc_msgSend
  • cmp p0, #0判断消息接受者是否为空

  • b.le LNilOrTagged cmp的结果是小于等于0则执行LNilOrTagged判断是否为 tagged pointer 小对象类型

  • b.eq LReturnZero``cmp结果 等于 0 执行地址LReturnZero 否则往下

  • dr p13, [x0] 将[x0]数据写入到寄存器中 即:p13寄存器保存了 对象的isa

  • GetClassFromIsa_p16 p13, 1, x0 isa & ISA_MASK 得到class

.macro GetClassFromIsa_p16 src, needs_auth, auth_address /* note: auth_address is not required if !needs_auth */

#if SUPPORT_INDEXED_ISA 
	// Indexed isa
	mov	p16, \src			// optimistically set dst = src
	tbz	p16, #ISA_INDEX_IS_NPI_BIT, 1f	// done if not non-pointer isa
	// isa in p16 is indexed
	adrp	x10, _objc_indexed_classes@PAGE
	add	x10, x10, _objc_indexed_classes@PAGEOFF
	ubfx	p16, p16, #ISA_INDEX_SHIFT, #ISA_INDEX_BITS  // extract index
	ldr	p16, [x10, p16, UXTP #PTRSHIFT]	// load class from array
1:

#elif __LP64__
//needs_auth = 1 不满足
.if \needs_auth == 0 // _cache_getImp takes an authed class already   
	mov	p16, \src
.else
	// 64-bit packed isa
        //  src为isa
	ExtractISA p16, \src, \auth_address   
.endif
#else
	// 32-bit raw isa
	mov	p16, \src

#endif

.endmacro

.macro ExtractISA
	and    $0, $1, #ISA_MASK // $1(isa) & ISA_MASK = $0(p16)即为class
.endmacro

CacheLookup函数解析

传入参数: NORMAL, _objc_msgSend, __objc_msgSend_uncached

//Mode:NORMAL , Function:_objc_msgSend , MissLabelDynamic : __objc_msgSend_uncached , MissLabelConstant 为空
.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
        //模拟器环境或mac
#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
        //#define CACHE            (2 * __SIZEOF_POINTER__) ,
        //x16(class)平移CACHE(16)位得到cache,首地址指向_bucketsAndMaybeMask,唧p11=_bucketsAndMaybeMask
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
        //64位真机环境
#if CONFIG_USE_PREOPT_CACHES
        // A12后处理器
#if __has_feature(ptrauth_calls)
	tbnz	p11, #0, LLookupPreopt\Function
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else   //A12以前设备
        // p11 & 0x0000fffffffffffe得到buckets
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
        //判断 p11第0位是否不为0,为0继续下面流程
	tbnz	p11, #0, LLookupPreopt\Function
#endif
        //p12 = p1(_cmd)^(p1(_cmd) >> 7)
	eor	p12, p1, p1, LSR #7
        // p12 得到方法缓存的下标 对应cache_t->insert->cache_hash函数
	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#else   
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#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
        p10 = buckets, p12 = index(第一次查询的index)
        一个bucket_t占用16字节(sel,imp两个指针)
        p12(index)左移4(= 1 + PTRSHIFT)位就是index * 16
        其实就是buckets首地址加上index个bucket_t内存大小,
        找到index位置的bucket,赋值给p13
        p13 = p10 + (p12 << 4) = buckets + index * 16,内存平移
        p13 = index位置的bucket
        */
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {
        /**
        #define BUCKET_SIZE      (2 * __SIZEOF_POINTER__)
        x13平移-BUCKET_SIZE,到下个bicket得到 p17(imp),p9(sel)
        */
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
        //比较 p9和_cmd
	cmp	p9, p1				//     if (sel != _cmd) {
        //不相对执行 3: 开始下次循环
	b.ne	3f				//         scan more
						//     } else {
        //相等,找到缓存
2:	CacheHit \Mode				// hit:    call or return imp
						//     }
        //cbz : 和 0 比较,如果结果为零就转移(只能跳到后面的指令)
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
        //b.hs:判断是否无符号小于,满足执行1:
	cmp	p13, p10			// } while (bucket >= buckets)
	b.hs	1b

	// 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
        //得到最后一个bucket
	add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
						// p13 = buckets + (mask << 1+PTRSHIFT)
						// see comment about maskZeroBits
#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
        //得到第一次查询的bucket
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
						// p12 = first probed bucket

						// do {
4:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
	cmp	p9, p1				//     if (sel == _cmd)
	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

#if CONFIG_USE_PREOPT_CACHES
#if CACHE_MASK_STORAGE != CACHE_MASK_STORAGE_HIGH_16
#error config unsupported
#endif
LLookupPreopt\Function:
#if __has_feature(ptrauth_calls)
	and	p10, p11, #0x007ffffffffffffe	// p10 = buckets
	autdb	x10, x16			// auth as early as possible
#endif

	// x12 = (_cmd - first_shared_cache_sel)
	adrp	x9, _MagicSelRef@PAGE
	ldr	p9, [x9, _MagicSelRef@PAGEOFF]
	sub	p12, p1, p9

	// w9  = ((_cmd - first_shared_cache_sel) >> hash_shift & hash_mask)
#if __has_feature(ptrauth_calls)
	// bits 63..60 of x11 are the number of bits in hash_mask
	// bits 59..55 of x11 is hash_shift

	lsr	x17, x11, #55			// w17 = (hash_shift, ...)
	lsr	w9, w12, w17			// >>= shift

	lsr	x17, x11, #60			// w17 = mask_bits
	mov	x11, #0x7fff
	lsr	x11, x11, x17			// p11 = mask (0x7fff >> mask_bits)
	and	x9, x9, x11			// &= mask
#else
	// bits 63..53 of x11 is hash_mask
	// bits 52..48 of x11 is hash_shift
	lsr	x17, x11, #48			// w17 = (hash_shift, hash_mask)
	lsr	w9, w12, w17			// >>= shift
	and	x9, x9, x11, LSR #53		// &=  mask
#endif

	ldr	x17, [x10, x9, LSL #3]		// x17 == sel_offs | (imp_offs << 32)
	cmp	x12, w17, uxtw

.if \Mode == GETIMP
	b.ne	\MissLabelConstant		// cache miss
	sub	x0, x16, x17, LSR #32		// imp = isa - imp_offs
	SignAsImp x0
	ret
.else
	b.ne	5f				// cache miss
	sub	x17, x16, x17, LSR #32		// imp = isa - imp_offs
.if \Mode == NORMAL
	br	x17
.elseif \Mode == LOOKUP
	orr x16, x16, #3 // for instrumentation, note that we hit a constant cache
	SignAsImp x17
	ret
.else
.abort  unhandled mode \Mode
.endif

5:	ldursw	x9, [x10, #-8]			// offset -8 is the fallback offset
	add	x16, x16, x9			// compute the fallback isa
	b	LLookupStart\Function		// lookup again with a new isa
.endif
#endif // CONFIG_USE_PREOPT_CACHES

.endmacro

解析:

  • mov x15, x16 将isa存储到x15
  • CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16真机环境
  • ldr p11, [x16, #CACHE] x16(class)平移CACHE(16)位得到cache,首地址指向_bucketsAndMaybeMask,即p11=_bucketsAndMaybeMask
  • CONFIG_USE_PREOPT_CACHES 64位真机环境__has_feature(ptrauth_calls)A12后优化,本文不做分析
  • and p10, p11, #0x0000fffffffffffe p11 & mask 得到 buckets
  • tbnz p11, #0, LLookupPreopt\Function判断 p110位是否不为0,为0继续下面流程
  • eor p12, p1, p1, LSR #7p12 = p1(_cmd)^(p1(_cmd) >> 7)
  • and p12, p12, p11, LSR #48 p12 得到方法缓存的下标 index 对应cache_t->insert-> cache_hash函数
  • add p13, p10, p12, LSL #(1+PTRSHIFT) 64位#define PTRSHIFT 3,p10 = buckets, p12 = index(第一次查询的index),一个bucket_t占用16字节(sel,imp两个指针),p12(index)左移4(= 1 + PTRSHIFT)位就是index * 16,其实就是buckets首地址加上indexbucket_t内存大小,找到index位置的bucket,赋值给p13 即:p13 = p10 + (p12 << 4) = buckets + index * 16。通过内存平移内存平移 p13 = index位置的bucket
  • 1:ldp p17, p9, [x13], #-BUCKET_SIZE 64位 #define BUCKET_SIZE      (2 * __SIZEOF_POINTER__) x13平移-BUCKET_SIZE,到下个bicket得到 p17(imp),p9(sel)
  • cmp p9, p1,判断p9p1是否相等,相等执行2:2: CacheHit \Mode,不相等执行3: cbz p9, \MissLabelDynamic
  • 2: CacheHit \Mode找到缓存,执行CacheHit``Mode=Noamal
  • 3: cbz p9, \MissLabelDynamic p9(sel)和 0 比较,如果结果为零就执行MissLabelDynamic(传的参数 __objc_msgSend_uncached)b.hs 1b:判断上面比较的结果是否无符号小于,为真则执行,cmp p13, p10比较,b.hs 1b,如果满足p13 >= p10继续执行循环1:ldp p17, p9, [x13], #-BUCKET_SIZE
  • 如果循环执行完成依然没有查询到缓存则继续下面的流程,按架构进入CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
  • add p13, p10, p11, LSR #(48 - (1+PTRSHIFT)) 得到最后一个bucket
  • add p12, p10, p12, LSL #(1+PTRSHIFT) 得到第一次查询的bucket
  • 4: ldp p17, p9, [x13], #-BUCKET_SIZE依次遍历,并将imp存储到p17sel存储到p9cmp p9, p1比较,p9(sel)p1(_cmd),b.eq 2b比较相等,执行2b(查找到缓存)cmp p9, #0判断p9(sel)是否为空,ccmp p13, p12, #0, ne比较p13(bucket)p12(first_probed),如果p13p12都 > 0 p13 > p12执行4b继续循环。
  • 查询不到执行MissLabelDynamic(__objc_msgSend_uncached)

部分架构判断图解:

image.png

image.png

CacheHit解析:

传入参数 NORMAL

// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
        // x17=imp,x10 = bucket,x1 = _cmd x16 = class
	TailCallCachedImp x17, x10, x1, x16	// authenticate and call 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
.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, 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

参数 $0 = NORMAL 执行TailCallCachedImp x17, x10, x1, x16,参数分别为x17=imp,x10 = bucket,x1 = _cmd x16 = class

TailCallCachedImp解析:

.macro TailCallCachedImp
	// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
        // imp = imp ^ cls ,对应 `bucket_t``encodeImp`函数`(uintptr_t)newImp ^ (uintptr_t)cls;`
	eor	$0, $0, $3
        // 跳转到imp 执行
	br	$0
.endmacro

__objc_msgSend_uncached解析:

STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves

![方法快速查找.drawio.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b8eb8c4e661f4f149b8ae6c0c717efd9~tplv-k3u1fbpfcp-watermark.image?)
// 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
        // 进入lookUpImpOrForward函数 参数 x0 receiver(self) x1 _cmd , x2 class, x3 = 3
	bl	_lookUpImpOrForward

	// IMP in x0
	mov	x17, x0

	RESTORE_REGS MSGSEND

.endmacro

解析:跳转到lookUpImpOrForward函数,传入receiver(self)_cmdclass3lookUpImpOrForward函数后续发文分析。

三、缓存查找流程图:

方法快速查找.drawio.png

总结: 方法的快速查找流程是通过调用 objc_msgSendreceiver取出 class,遍历 cache 缓存的 bucket,依次取出bucketsel_cmd进行比较并判断执行的过程。