IOS底层探索CacheLookup汇编分析

1,107 阅读6分钟

上篇objc_msgSend汇编分析得到了class,接着开始查找缓存的分析

WX20210629-142531@2x.png

CacheLookup汇编分析

     //CacheLookup LOOKUP, _objc_msgLookup, __objc_msgLookup_uncached
.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
	//
   //将class移到x15里面
	mov	x15, x16			// stash the original isa
LLookupStart\Function:
	// p1 = SEL, p16 = isa
//TARGET_OS_OSX/TARGET_OS_SIMULATOR
#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
//真机arm64
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//#define CACHE            (2 * __SIZEOF_POINTER__)
//[x16, #CACHE]将x16(isa)平移CACHE大小 (CACHE是2倍指针的大小就是16)得到cache_t
//ldr    p11, cache_t 将cache_t放到p11,即p11 = cache_t
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
//CONFIG_USE_PREOPT_CACHES:arm64 && ios && 不是模拟器 && 不是mac
#if CONFIG_USE_PREOPT_CACHES
//iphoneX及以后设备
#if __has_feature(ptrauth_calls)
//将p11与0比较,如果不为0就执行LLookupPreopt\Function
	tbnz	p11, #0, LLookupPreopt\Function
//将p11(cache_t)的首地址 & 0x0000ffffffffffff 得到buckets  给 p10
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
   //将p11(cache_t)的首地址 & 0x0000fffffffffffe 得到buckets 给 p10
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
//将p11与0比较,如果不为0就执行LLookupPreopt\Function
	tbnz	p11, #0, LLookupPreopt\Function
#endif
//下面是p11为0的操作
//p0是receiver,p1是sel
//eor 逻辑异或(^)
//将p1(sel)右移7个位置后 ^ sel后,给p12
	eor	p12, p1, p1, LSR #7
//p11(cache_t)首地址右移48位后 & p12 后 再给到p12
	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
  // p11, LSR #48    p11(cache_t)首地址右移48位后得到mask
  //p1(sel) & mask 得到index 存到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
// p11 cache -> p10 = buckets
// p1(_cmd) & mask = index -> p12
// #define PTRSHIFT 3
// p12(index)左移(PTRSHIFT+1),即左移4位 后给p12
//add    p13, p10, p12 : buckets + 左移4位的(p12) 后 给到 p13
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {
//将*bucket--里面的东西分别存在p17和p9中
//(*bucket--)是拿到bucket里面的东西 imp(p17)  sel(p9)
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
//查到的sel和传入的sel进行比较是否相同,如果相同,就会走到 2 里面,不相同就会走到 3f 里面,其实就是循环查找
	cmp	p9, p1				//     if (sel != _cmd) {
	b.ne	3f				//         scan more
						//     } else {
//缓存命中 Mode就是传过来的NORMAL
2:	CacheHit \Mode				// hit:    call or return imp
						//     }
//p9 为空
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
//p13 和 p10(buckets首地址)对比 ,如果p13大于等于p10,就回到1,再次*bucket--
	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
//共享缓存里面也可能存在 给p12
	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
// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
 如果 $0 等于 NORMAL就会调用TailCallCachedImp
	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
.macro TailCallCachedImp
	// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
   // x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
  // eor 逻辑异或(^)
  // $0^$3 = x17 ^ x16 = IMP ^ class 结果再给到$0。IMP编码
	eor	$0, $0, $3
//调用IMP
	br	$0

CacheLookup的逻辑大概如下:

  1. isa首地址平移16个字节大小,得到cache_t,在objc_class结构体中,cache前面还有isasuperclass各占8个字节。
  2. cache_t的首地址与上0x0000fffffffffffe 得到buckets
  3. cache_t0比较,如果不为0就执行LLookupPreopt\Function,如果为为0,执行第4
  4. _objc_msgSend传入的第二个参数SEL与上maskcache右移48位得到mask)得到bucket的下标index
  5. buckets首地址+偏移量(下标index左移(PTRSHIFT+1)位,PTRSHIFT=3)取出bucket
  6. do-while循环,当bucket地址小于buckets首地址时循环结束。循环下面操作:
  • 6.1 每次按BUCKET_SIZE大小将地址减小

  • 6.2 取出bucket中的SELobjc_msgSend传入的SEL进行比较是否相同

  • 6.3 如果相同,就会缓存命中,执行CacheHitIMP = IMP^ISA得到IMP并调用

  • 6.4 如果不相同就判断在bucket取出的SEL是否为空,为空就执行MissLabelDynamic,否则就将bucket地址每次按BUCKET_SIZE大小减小,向前查找。

  • 6.5 如果循环结束还没找到就执行__objc_msgSend_uncached

objc_msgSend及CacheLookup流程图

111.png

objc_msgSend及CacheLookup流程图总结

objc_msgSend的流程就是通过sel拿到imp,并调用imp。流程如下:

  1. 判断recevier是否存在
  2. 通过recevierisa得到class
  3. class内存平移拿到cache,其中包含了bucketmask
  4. cache首地址与上0x0000fffffffffffe得到buckets
  5. cache首地址右移48位得到mask。需要mask是因为insert哈希函数是这样计算的:(mask_t)(value & mask)
  6. 获得第一次查找的indexindex = SEL&mask
  7. bucket+index整个缓存里面的第几个bucket
  8. 取出bucket中的sel
  9. selobjc_msgSend传入的sel进行比较,相等就命中缓存,调用IMP
  10. 不相等就让bucket--,回到第7步开始循环
  11. 如果循环结束还没找到就执行__objc_msgSend_uncached

补充

图片.png 如图,第一个红框value0,当调用一个方法时[p saySomething](第二个红框),value7,按照之前对Cache_t探索,初始容量是4,当容量大于当前容积的3/4,就会进行扩容。那么在调用saySomething时,还有哪些方法占着容量呢?

cache_t::insert方法中 unsigned oldCapacity = capacity(), capacity = oldCapacity;后面插入以下代码:

if (sel == @selector(saySomething)) {
        bucket_t *kc_b = buckets();
        for (unsigned i = 0; i<oldCapacity; i++) {
            SEL kc_sel = kc_b[i].sel();
            IMP kc_imp = kc_b[i].imp(kc_b,nil);
            printf("%p - %p - %p\n",kc_sel,kc_imp,&kc_b[i]);
        }
        printf("isConstantEmptyCahe %p - %u - %u -%u\n",kc_b,capacity,newOccupied,oldCapacity);
    }

插入这段代码的目的是在调用saySomething方法的时候,打印出当前占用容量的方法。lldb调试结果如下: 图片.png 前两个分别是respondsToSelector:class,第三个是空的。第四个imp地址和第一个的bucket地址是同一个地址都是0x10144d050。在allocateBuckets函数中, 图片.png 意思是会在列表末端添加一个额外的bucketsel1,并指向第一个bucket

回到之前的问题,为什么value7

因为当前有四个方法,大于当前容积(默认4)的3/4,所以扩容了。