iOS 底层探索08——iOS方法缓存(下)

941 阅读6分钟

这是我参与更文挑战的第10天,活动详情查看: 更文挑战

前言

我们已经探究过方法缓存的插入流程方法缓存的触发时机、今天继续从汇编层面对方法缓存进行分析;

objc_msgsend、Cache 相关细节

objc_msgsend汇编相关方法细节

  1. .macro GetClassFromIsa_p16 src, needs_auth, auth_address获取当前对象的class
  2. .macro ExtractISAisa & isa_mask;

cache 读写过程细节

  1. 读取过程:objc_msgsend->_lookUpImpTryCache->cachegetIMP
  2. 写入过层:log_fill_cache->insert();

LLDB调用方法特殊细节

  1. LLDB调用方法的时候,按照我们的猜测,调用1次teacherSay方法则 occpuied=1_maybeMask = 3;但实际上occpuied=1_maybeMask = 7;我们都知道_maybeMask = capacity - 1;而初始的capacity=4,这里capacity = 8肯定是经过insert之后扩容过的;
  2. 为了查找什么时候执行insert方法的,我们在insert中进行打印输出,最后发现LLDB调试的时候, 调用1次 teacherSay 方法时会额外再进行了2次 inser操作,分别是insert respondsToSelectorinsert class,所以当insert teacherSay 的时候,发现已经达到扩容条件了,就开始扩容为8了;
  3. 在分析额外insert的2个方法的时候,我们发现另外一个点:系统会在reallocate->allocateBuckets中,默认把当前bucket的地址值插入进来作为一个元素,这也是为什么我们比较size的时候要+1,以及为什么maybeMask=capacity-1的原因;
//打印代码
printf("\n%s-------%p---------%p----end\n",(char *)sel,imp,receiver);
LLDB调试源码
(lldb) po [p teacherSay]
respondsToSelector:-------0x100338dd0---------0x101005750----end
class-------0x100338a30---------0x101005750----end
teacherSay-------0x1000037b0---------0x101005750----end
2021-06-27 11:08:32.013613+0800 KCObjcBuild[85513:7316344] -[GCTeacher teacherSay]

retain-------0x1003397a0---------0x1006a0e70----end

release-------0x100339930---------0x1006a0e70----end

dealloc-------0x100339ab0---------0x1006a0e70----end
(lldb) 

bucket_t::set()细节;

  1. 当当前缓存可以插入的时候调用bucket_t::set()方法,此时在arm架构下先存imp后存sel;在x86架构下先存sel后存imp
  2. arm真机下先存imp可能是考虑首地址放imp减少查找时的开销;
struct bucket_t {
private:
    // IMP-first is better for arm64e ptrauth and no worse for arm64.
    // SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
    explicit_atomic<uintptr_t> _imp;
    explicit_atomic<SEL> _sel;
#else
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
#endif
}

二次hash函数cache_next细节

二次hash的具体逻辑看下面方法注释;

cache_next(i, m)//再次哈希使用的是当前的位置和容量-1作为参数进行cache_next计算

static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;//如果i不为0,返回i-1,否则返回mask(容量-1);也可以理解为判断发生冲突的位置是不是在buckets的最开头,如果不在最开头就直接前移,如果在最开头就直接跳到容量-1的位置,再依次向前,直到再次遇到一开始的begin位置,此时说明循环了一圈了还没找到空位置插,坏缓存了;
}

objc_msgSend 汇编

  1. _objc_msgSend主流程汇编源码
ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame

	cmp	p0, #0			// nil check and tagged pointer check// p0为消息接受者、这里比较p0和#0
#if SUPPORT_TAGGED_POINTERS  //判断是否支持Taggedpointer类型
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)//如果支持Taggedpointer类型按照LNilOrTagged处理
#else
	b.eq	LReturnZero  // 返回nil:p0和#0相等时,如果不支持Taggedpointer类型返回zero
#endif
	ldr	p13, [x0]		// p13 = isa,[x0]存放的是class
    //src, needs_auth, auth_address
    //x0 和 p13此时都是isa的地址;
	GetClassFromIsa_p16 p13, 1, x0	//从 GetClassFromIsa_p16 中获取clsss:p16 = class
// receiver->class 获取class,去class 中找method cache
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
  1. 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 当前架构不支持
	// 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__ //真机为64位架构
.if \needs_auth == 0 // _cache_getImp takes an authed class already
	mov	p16, \src//把isa存到P16
.else
	// 64-bit packed isa
	ExtractISA p16, \src, \auth_address // and    $0, $1, #ISA_MASK 这里相当于获取class
.endif
#else
	// 32-bit raw isa
	mov	p16, \src

#endif

.endmacro
  1. CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached汇编源码分析
//CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached,MissLabelConstant
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
//此时x16存放的是class对象,把class对象移动到x15
	mov	x15, x16			// stash the original isa
//传入LLookupStart的Function参数为_objc_msgSend
LLookupStart\Function:
	// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS// arm64位真机下相等
	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//KC说的真机走这里
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets//#CACHE是一个宏定义值为2倍的指针大小即16(2 * __SIZEOF_POINTER__),这行代码作用是将x16即class对象平移16得到cache_t的地址,然后放到p11,即p11 = cache_t的地址
    #if CONFIG_USE_PREOPT_CACHES//这里CONFIG_USE_PREOPT_CACHES==1所有走下面一行
        #if __has_feature(ptrauth_calls)//这里是arm64e所以走else
            tbnz	p11, #0, LLookupPreopt\Function
            and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
        #else
            //p11是cache的地址,这个地址下的第一个成员就是bucketsandmask的首地址,所以这里p11相当于bucketsandmask的地址,在当前的架构(CACHE_MASK_STORAGE_HIGH_16)下cache中定义的时候maskshift = 48位;maskzerobits = 4;bucketsMask = maskshift- maskzerobits-1;bucketsMask的定义在cache中也可以看到;
            //取出cache中的bucketsandmask &0x0000fffffffffffe即取前48位,其中47位都为1最后一位为0,意思就是不取最后一位;最终取出的是buckets,然后放进p10中,即p10 = buckets;
            and	p10, p11, #0x0000fffffffffffe

            //tbnz:不为0就跳走,否则走后面的LLookupPreopt\Function
            //判断p11与#0做比较,判断p11是否为空;如果p11为空,即bucketsandmask不存在,说明没有缓存就向下一步继续执行,否则走LLookupPreopt\Function;
            tbnz	p11, #0, LLookupPreopt\Function
        #endif
        //p0此时为第一个参数:消息接受者receiver;
        //p1此时为第二个参数:_cmd,即sel;
        //如果bucketsandmask为空则将p1按位右移7位;
        eor	p12, p1, p1, LSR #7
        and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
    #else
        //此时p11=cache_t,cache_t & #0x0000ffffffffffff = buckets()
        and	p10, p11, #0x0000ffffffffffff	// p10 = buckets

        //步骤1.p11(buckets中的bucketsandmask)右移48位得到mask的值;
        //步骤2.将p1(sel) & mask(步骤1生成的)的结果buckets存入p12;
        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
    //p12当前方法的buckets;
    //PTRSHIFT = 3;
    //p12为什么要左移?因为bucket 平移的时候只能平移1/2/3/4个单位,不能直接平移到某个地址;
    //步骤1 p12左移#(1+PTRSHIFT)个位置得到的值 + p10得到的值放在p13 即p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT)),p13:当前查找的bucket
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {
//ldp 取当前偏移的地址同时存到p17和p9
// bucket里面的东西:imp(p17) sel(p9)
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
//比较我们要查找的sel和p9的值
	cmp	p9, p1				//     if (sel != _cmd) {
//如果不等于,跳转3f
	b.ne	3f				//         scan more
						//     } else {
2:	CacheHit \Mode				// hit:    call or return imp
						//     }
//如果p9为空则,查找的对象为nil 则 MissLabelDynamic
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
//p10 为第一个bucket的位置,p13为当前的bucket,用p10和p13对比,如果p13较大就向前平移,然后继续查找
	cmp	p13, p10			// } while (bucket >= buckets)
	b.hs	1b//如果p13

objc_msgSend汇编流程总结

objc_msgSend汇编分析.png