iOS 底层探索篇 ——Runtime-objc_msgSend流程分析 - 快速查找流程

314 阅读6分钟

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

补充:在lldb中调用方法为什么mask为7

我们看到在代码中调用方法的情况,那么在lldb中调用方法呢?试一下:

在这里插入图片描述

这里的mask变成了7,这是为什么呢? mask变成了7,那么代表着,cache一定进行了一次类似的扩容,那么其必然就会到insert里面,那么我们去insert实现里面。

之前证明了,当缓存方法到了3/4 的时候,就会进行扩容,那么是不是我们在lldb中调用方法的时候,会同时调用其他方法呢,我们来看一下,在insert中打印一下sel,imp 和 receiver,这样就会打印所有的方法。运行一下。

在这里插入图片描述

然后在lldb中调用对象方法,看看输出。

在这里插入图片描述

我们想要找的情况是receiver是p的情况,所以我们打印一下p的地址。

在这里插入图片描述

然后在上面的打印结果中寻找,发现这三个方法的receiver都是p。

在这里插入图片描述

所以我们发现,在我们调用saySomething方法之前,会先调用 respondsToSelector以及class 方法,所以在插入saySomething的时候会进行扩容,所以mask变为7.

作业:代码运行方法会不会插入结束符号

代码中调用方法,这里调用两次 在insert里面把bucket的sel,imp和地址打印出来。

在这里插入图片描述

这里saySomething之前调用setName是因为上面的打印方法放在开辟空间之前,如果不先调用空间还没开辟打印不出来东西,因为oldCapacity会是0,运行一下。

在这里插入图片描述

打印结果可以看到,还是有结束符号的。

在这里插入图片描述

这里的0x7fff7c838d69是setName 方法,因为saySomething在下面才插进去。

接着看insert 里面的插入。

在这里插入图片描述

这里有一个do while 循环,上次说到这里,当插入的时候如果找到一个值,发现里面有人,就会进行cache_next。看一下cache_next内部是什么样的。

在这里插入图片描述

发现这里如果发现有值在里面的话,如果i不等0,就往前移一位重新插入,否则就会到mask开始重新插入,那么什么是mask呢。前面看到传进来的是m,往前找,发现m是:

在这里插入图片描述

大致流程图:

在这里插入图片描述

那么会不会有一种情况,就是一直找不到空位呢?如果一直找不到,那么就会退出循环,然后调用bad_cache。

    bad_cache(receiver, (SEL)sel);

再看一下set方法。

在这里插入图片描述

发现是先存imp,在存sel。

在这里插入图片描述

objc_msgSend流程

上文讲解了objc_msgSend 汇编源码,得到了以下流程:

  1. 判断receiver是否存在
  2. 通过receiver获取isa,进而获取class

现在已经获取到了class了,那么class要拿来干什么呢? 这里看到拿到了class之后,调用了 CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached 这个函数

在这里插入图片描述

接着我们去搜索一下CacheLookup 的实现。这里把之前的函数复制过来,一一对应发现少了一个参数,那么说明最后一个参数是默认参数。

在这里插入图片描述

往下看代码:

// 之前的p16是class,这里是把class放到x15里
mov	x15, x16			
LLookupStart\Function:
	// p1 = SEL, p16 = isa
#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,所以是CACHE_MASK_STORAGE_HIGH_16,看解释1.
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
// 把x16 平移#cache大小,也就是16个字节,看解释2.所以就得到cacht_T.
//然后把的到的结果放到p11,所以p11 = cache
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
#if CONFIG_USE_PREOPT_CACHES
#if __has_feature(ptrauth_calls)
	tbnz	p11, #0, LLookupPreopt\Function
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
// 接下来进入到这里,原因看解释3
#else
// p11 与上 #0x0000fffffffffffe得到bucket,放到p10里面。
//原因看解释4
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
//判断p11和0,不为0,也就是存在则跳转LLookupPreopt,否则往下走
	tbnz	p11, #0, LLookupPreopt\Function
#endif
// p11 不为0 走到这里
// p12位空,p1为sel,这里的操作是这样(_cmd ^ (_cmd >> 7)) 
	eor	p12, p1, p1, LSR #7
	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
// p11 是cache,与上0x0000ffffffffffff得到bucket
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
//p11, LSR #48	->mask
// p1:sel(_cmd) & mask 得到index存入p12. 原因看解释5
	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
// 
// p10是bucket,p12是index,PTRSHIFT是3,看解释6,所以就是index左移4位。因为bucket + 内存平移,只能平移 1,2,3,4单位,不能平移0x10XXXXX地址,所以((_cmd & mask)左移4位相当于index * 16(bucket一个单位的大小),这样就会得到在bucket中对应的地址。因此,p13就是当前要查找的bucket
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {
//从x13(即p13 buckets首地址)- 实际需要平移的内存大小BUCKET_SIZE,得到得到第二个bucket元素,imp-sel分别存入p17-p9,即向前查找
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
// 拿查到的sel p9 和 要查的 p1 对比
//一样就到CacheHit(缓存命中)
//不一样就到3里面
	cmp	p9, p1				//     if (sel != _cmd) {
	b.ne	3f				//         scan more
						//     } else {
2:	CacheHit \Mode				// hit:    call or return imp
						//     }
// 判断p9是否为空。
//为空就去MissLabelDynamic
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
// 对比p10 和 p13 大小,如果p13大于p10,就地址平移,也就是再次哈希的-1,然后从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平移 mask<<4 的结果的位数,然后放到p13,对应了在哈希的过程看解释7
	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

CacheHit(缓存命中)

.macro CacheHit
// LLookupStart参数为normal 进入这里
.if $0 == NORMAL
// x17为cached imp,x10是bucket的地址,x1是要查找的sel,x16是isa
// 进入到TailCallCachedImp,进行x17异或x16,看解释8然后执行imp
	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

MissLabelDynamic

如果找不到就会进入到MissLabelDynamic,也就是之前传过来的第三个参数__objc_msgSend_uncached

在这里插入图片描述

__objc_msgSend_uncached:流程

STATIC_ENTRY __objc_msgSend_uncached

	// THIS IS NOT A CALLABLE C FUNCTION
	// Out-of-band r9 is the class to search
	
	MethodTableLookup NORMAL	// returns IMP in r12
	bx	r12

	END_ENTRY __objc_msgSend_uncached

解释1

从这里可以看到,当是__arm64__(支持64位)且是__LP64__(unix或者unix类的系统-linux,Mac OS X的情况下,如果是#if TARGET_OS_OSX || TARGET_OS_SIMULATOR)(macOS或者macOS模拟器)的时候是CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS,不然就是CACHE_MASK_STORAGE_HIGH_16,所以这里是CACHE_MASK_STORAGE_HIGH_16。

在这里插入图片描述

解释2

在文件中搜索cache,发现他是2 * SIZEOF_POINTER 也就是16.

在这里插入图片描述

解释3

在源码中寻找CONFIG_USE_PREOPT_CACHES,发现其定义

在这里插入图片描述

因为这里是64位,而且是os系统,不是模拟器也不是MACCATALYST,所以CONFIG_USE_PREOPT_CACHES是1,进循环。 has_feature(ptrauth_calls): 是判断编译器是否支持指针身份验证功能,a12Z以上的芯片。所以这里是false。所以进下面的else。

解释4

mask和buckets放在一起共占用8个字节,64位。 0x0000fffffffffffe转化为2进制为 111111111111111111111111111111111111111111111110,是48位。 再来看cache_t的结构,maskShift也是48,第1到48位存放了maskshift,而bucketsMask是就存在了高端的16位。这里p11 cache & 0x0000fffffffffffe 将高16位mask抹零,得到了bucket。

在这里插入图片描述

解释5

我们看在insert是如何取第一个下标的,在源码中看到,insert里面是通过sel&mask得到的index。

在这里插入图片描述

在这里插入图片描述

解释6

在这里插入图片描述

解释7

这里看到如果i等于0的时候,那么就会直接取mask的值。

在这里插入图片描述

解释8

进入TailCallCachedImp流程如下

.macro TailCallCachedImp
	// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
	// x17异或isa(类)
	// call imp
	eor	$0, $0, $3
	br	$0
.endmacro

流程总结

  1. 判断receiver是否存在
  2. 通过receiver获取isa,进而获取class
  3. 进入 CacheLookup,通过class内存平移获取到cache
  4. 通过bucket掩码得到bucket
  5. 通过mask掩码得到mask
  6. 通过insert 哈希函数 (mask_t)(value&mask)获取到第一次查找的index
  7. bucket + index 找到 应该要找的那个bucket
  8. bucket里面有imp和sel,拿到查找到的sel和之前传进来的sel对比,相等就cacheHit,imp^isa解码,然后调用imp,否则就再次平移,找到就cacheHit,找不到就继续循环。如果一直没有找到就到__objc_msgSend_uncached。

流程图

在这里插入图片描述