objc_msgSend汇编分析

584 阅读3分钟

OC的方法调用,在底层使用的是objc_msgSend进行消息发送,那么objc_msgSend到底做了什么事情,今天来探索一下。在objc4-818的源码中,找到objc_msgSend的汇编源码。

        ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame

	cmp	p0, #0			// nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
	b.eq	LReturnZero
#endif
	ldr	p13, [x0]		// p13 = isa
	GetClassFromIsa_p16 p13, 1, x0	// p16 = class
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

objc_msgSend默认的两个参数是id self, sel _cmd,self保存在x0寄存器,_cmd保存在x1。进入方法,首先比较p0是否等于0,小于等于进入LNilOrTagged,小于0的话是tagged pointer(小对象),等于0进入LReturnZero,返回0,这是校验消息接收者。x0不为0继续往下走,x0中保存着对象的指针,p13取出指针指向的内容,正好是isa。

进入GetClassFromIsa_p16方法

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

#if SUPPORT_INDEXED_ISA    //armv7或者arm64_32
	// 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__      //Unix系列的系统,包括Linux,MacOS,iOS64位的
  .if \needs_auth == 0 // _cache_getImp takes an authed class already
	mov	p16, \src
  .else
	// 64-bit packed isa
	ExtractISA p16, \src, \auth_address
  .endif
#else
	// 32-bit raw isa
	mov	p16, \src

#endif

.endmacro

p13,1,x0作为参数分别赋值给了src, needs_auth, auth_address,首先判断是否SUPPORT_INDEXED_ISA指的是架构判断是否为armv7或者arm64_32,我们看真机的架构就可以了,就看arm64,就会进入ExtractISA p16, \src, \auth_address

.macro ExtractISA
	and    $0, $1, #ISA_MASK
.endmacro

ExtractISA里面就一个指令,$0 = $1 & ISA_MASK0p160是p16,1是src也是上一步传过来的p13,根据之前的代码,p13中保存的是对象的isa,这里的p16 = isa & isa_mask,就是取出了对象的类。

下面进入CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached流程,代码里会有不同架构以及大端小端的判断,为了看起来简洁,我们就取出所需的,架构为arm64,小端模式的代码来分析。

//CacheLookup 父类的快速查找 参数: GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant
//CacheLookup 查找自己的cache 参数:NORMAL, _objc_msgSend, __objc_msgSend_uncached
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant

	mov	x15, x16			// stash the original isa
LLookupStart\Function:
	// p1 = SEL, p16 = isa

#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES     // =1
   #if __has_feature(ptrauth_calls)  //A12处理器以及后,可以先不看这个
	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
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES


	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
	cmp	p9, p1				//     if (sel != _cmd) {
	b.ne	3f				//         scan more
						//     } else {
2:	CacheHit \Mode				// hit:    call or return imp
						//     }
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
	cmp	p13, p10			// } while (bucket >= buckets)
	b.hs	1b
	
        add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))   // p13 = buckets + (mask << 1+PTRSHIFT)
        
        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


通过class取出cache_t,然后经过一系列的&和平移运算,循环遍历cache中的sel,与调用的sel对比,如果找到了进入CacheHit mode是NORMAL,如果一直找不到就会进入MissLabelDynamic也就是__objc_msgSend_uncached

// 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
.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
.macro TailCallCachedImp
	// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
	eor	$0, $0, $3       //按位&运算
	br	$0
.endmacro

通过运算,拿到cache中的imp并且返回。

总结:objc_msgSend所做的事情就是,首先进行快速查找,找不到则进行慢速查找。

  1. 通过对象找到类
  2. 从类的首地址,平移16字节,找到cache
  3. 遍历cache中的sel,与调用的sel比较,找到则返回,调用对应imp。找不到,则调用__objc_msgSend_uncached进入慢速查找。