OC底层原理7之cache中的insert流程以及objc_msgSend汇编快速缓存查找

306 阅读13分钟

本章内容

  1. 方法在什么时候才开始进行insert呢。
  2. 补充知识runtime的三种发起方式调用底层
  3. objc_msgSend的汇编源码分析,以及查找流程

查看insert方法流程

我们为什么要看方法insert流程,这是因为一个方法的读取必定要先进行插入才可以,就像objc_msgSend一样它的作用就是为消息的接收者找到那个方法,那么为了进行性能考虑,我们是否可以猜想它是否一定是先从缓存中找到呢。但是如果说第一次呢?是不是就在缓存中找不到了,那么为了第二次更快的查找它会不会在第一次就进行了insert呢?

objc断点查看insert的方法调用栈

总结:我们根据这一小节可以暂时得到insert的流程是:_objc_msgSend_objc_msgSendSuper2 --> _objc_msgSend_uncached --> lookUpImpOrForward --> log_and_fill_cache --> insert

注意:我们是从最内向外一层层递进(也就是说正确顺序是6->1,但是我们是逆推的)。我们最终的目的是为了找到objc_msgSend,其实是它我们才会进行方法的缓存。然后汇编查找是arm64的架构文件

  1. 我们已经知道 栈 是先进后出的流程,也就是说我们根据这个图可以知道insert之前方法执行流程依次是: _objc_msgSend_uncached --> lookUpImpOrForward --> log_and_fill_cache --> insert image.png

  2. 先去查看 log_and_fill_cache 源码,其实对于我们来说这个方法根本就没什么用

image.png

  1. 查看 lookUpImpOrForward 源码,我们找到了它调用 log_and_fill_cache的地方。 image.png

  2. 查看方法 _objc_msgSend_uncached源码,我们发现他其实是用汇编撰写的。而单看汇编我们没有发现它调用lookUpImpOrForward方法,却发现了MethodTableLookup方法

image.png

  1. 查找MethodTableLookup方法后我们发现是它调用了lookUpImpOrForward方法,但是又是谁调用了_objc_msgSend_uncached呢? image.png

  2. 我们又根据_objc_msgSend_uncached进行查找发现了整个文件有两个地方调用它一个是_objc_msgSend,一个是_objc_msgSendSuper2中调用了方法CacheLookup传入了_objc_msgSend_uncached参数。两个基本都差不多所以_objc_msgSendSuper2不在贴出来。

image.png

调起runtime底层的三种方式

runtime有两个版本,一个是Legacy版本(早起版本)对应的编程接口是Objective-C 1.0,一个是Modern版本(现行版本)对应的编程接口是Objective-C 2.0。

运行时

编译时:正在编译的时候,就是代码没有被装载到内存的时候。这个时候编译器帮我们做一些语法分析等一些常规处理比方说语法错误就是在这个时候会被分析出来,然后项目就不能编译通过。

运行时:就是代码已经跑起来,被装载到内存中了。例如一个类声明了一个方法却没有实现,但是你又偏偏调用这个类,你编译项目(build)却能构建成功,但是如果你运行(run)的话就会崩溃。

runtime的层级

  1. OC代码层
  2. NSObject等服务层和runtime的接口层
  3. 编译器,负责翻译,将上层代码转成中间层例如clang的时候
  4. runtime的底层库 image.png

调起runtime底层方式:

  1. 直接在我们常使用的OC层面去调起,例如调用方法等
  2. 通过NSObject层调用其api,例如 isKindOfClass等
  3. 底层提供的objc的api,例如 class_getInstanceSize

举例说明

1、我们根据下图可以得知,我们任何调用方法的过程其实就是消息发送的过程。 objc_msgSend(消息接收者,消息的主体)。

image.png image.png

2、例如我们可以根据用objc_msgSend方法进行调用方法。

注意:如果说要调用objc_msgSend方法需要在 Build Settings -> Enable Strict Checking of objc_msgSend Calls 地方改为NO

//该方法在类Person中
-(void)personWithAge:(int )age withName:(NSString *)name
{
    NSLog(@"----%d-----%@", age, name);
}
//
Person *p = [Person alloc];
objc_msgSend(p, @selector(personWithAge:withName:), 18, @"哈哈");

输出结果为:----18-----哈哈

3、例如调用objc_msgSendSuper(objc_super *, 消息主体)方法去调用,Teacher类是继承自Person类,而Teacher对象去调用Person类的方法

//该方法在类Person中
-(void)personWithAge:(int )age withName:(NSString *)name
{
    NSLog(@"----%d-----%@", age, name);
}
//
Teacher *t = [Teacher alloc];
// 结构体objc_super
struct objc_super p_objc_super;
p_objc_super.receiver = t;
//这个是第一次方法查找的类,例如你如果传Teacher.class的话
//方法查找流程是 Teacher -> Person(假如没找到) -> NSObject
p_objc_super.super_class = Person.class;
objc_msgSendSuper(&p_objc_super, @selector(personWithAge:withName:), 16, @"嘻嘻")
        

输出结果为:----16-----嘻嘻

objc_msgSend调用分析

我们任何方法的调用都离不开消息的发送,但是我们发现objc源码中objc_msgSend是用汇编写的,这是什么原因呢?

  1. 在C语言中不可能通过写一个函数来保留未知参数并且跳转到一个任意函数指针。C语言没有满足做这件事情的必要特性
  2. 我们程序中有大量调用方法的代码,这就必要保证objc_msgSend需要更快去处理

注意:本次分析是分析真机模式下的,也就是arm64架构

objc_msgSend缓存命中

我们分析的是arm64架构的源码,而真机下(说的是64位)的寄存器是x0->x28,29个寄存器,每个寄存器可存放8字节。x0 - x7 用作函数的参数传递, x0 经常被用作函数返回值。

objc_msgSend源码

方法做了什么

  1. 看p0(receiver消息接收者)是否存在,如果存在则2流程,如果不存在则查看是否为arm64架构如果是则执行LNilOrTagged方法否则LReturnZero
  2. 获取receiver的isa值,给寄存器p13。继续3流程
  3. 调用GetClassFromIsa_p16 然后得到 p16为 receiver的Class。继续4流程
  4. 调用CacheLookup 方法,然后去寻找方法,如果缓存中有方法则缓存命中,没有的话就执行_objc_msgSend_uncached

源码

	ENTRY _objc_msgSend  //进入objc_msgSend方法
	UNWIND _objc_msgSend, NoFrame //创建一个窗口,不重要

	cmp	p0, #0 //查看p0(receiver)是否为nil,如果为nil进行下一步不然跳过,cmp是比较指令,p0是消息的接收者
#if SUPPORT_TAGGED_POINTERS //是否支持taggedPointer类型,如果是arm64为1。
	b.le	LNilOrTagged //b是跳转指令
#else
	b.eq	LReturnZero //b是跳转指令
#endif
	ldr	p13, [x0] //将isa给p13,ldr赋值指令,将x0(receiver)的地址数据给p13,而x0的地址对应的内存数据是isa
	GetClassFromIsa_p16 p13, 1, x0	//获取receiver的Class,将isa对应的类地址(Class)给p16。传入参数:p13(isa),1,x0(receiver)。补充内容:至于为什么要找对象的类呢请看这个方法我给出的解释
LGetIsaDone:
	// calls imp or objc_msgSend_uncached
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached // 调用方法CacheLookup,去找缓存,如果找到缓存命中没找到则走_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. cmp 比较指令文中有解释
  2. b 跳转指令
  3. ldr赋值指令,是将后面的内存地址中指向的内存数据给前一个。
  4. LGetIsaDone是已近完成了,然后继续走下面的代码

GetClassFromIsa_p16 方法分析

该方法其实就是isa平移去得到Class的过程。至于objc_msgSend为什么要找到对象的类呢,其实最主要的是因为它要找到类的cache,也就是缓存的方法。

我们从objc_msgSend源码得知它调用了该方法GetClassFromIsa_p16 p13, 1, x0,而我们则得知 src 为 p13(isa),needs_auth 为1, auth_address 为 receiver (消息接收者例如对象p)。然后将Class值给在p16里面

源码

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

#if SUPPORT_INDEXED_ISA //如果说是arm64且不是64位 为1
	// Indexed isa
	mov	p16, \src //p16为isa,mov代表 p16 = src
	tbz	p16, #ISA_INDEX_IS_NPI_BIT, 1f // tbz代表一个位运算就是将p16的值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__  //如果是MacOS系统
.if \needs_auth == 0 // _cache_getImp takes an authed class already
	mov	p16, \src
.else //最终走这里
	// 64-bit packed isa 
        //该方法就是将isa & isaMask,也就是得到了Class,p16 = Class
	ExtractISA p16, \src, \auth_address
.endif
#else
	// 32-bit raw isa
	mov	p16, \src

#endif

.endmacro

CacheLookup 方法分析

  1. 我们首先要明白这个方法在经过objc_msgSend调用的时候的几个参数的值: Mode = NORMAL, Function = _objc_msgSend, MissLabelDynamic = __objc_msgSend_uncached,MissLabelConstant = MissLabelConstant
  2. 寄存器的值:p13 = isa, p16= Class
  3. CACHE_MASK_STORAGE 是一个宏定义在 defined(__arm64__) && __LP64__(意识是真机而且是arm64架构而且是64位) 里面则是CACHE_MASK_STORAGE_HIGH_16 或 CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS,但我们是真机模式则应该结果为CACHE_MASK_STORAGE_HIGH_16

本方法做了什么

  1. x15 = Class
  2. 将Class平移16字节给p11,也就是p11 = 类的cache,而cache结构体第一个值为_bucketAndMaybeMask。所以最终为:p11 = _bucketAndMaybeMask
  3. 根据 p11 获取 buckets(),也就是 p10 = buckets;
  4. 看 p11 是否为0,不为0的话走LLookupPreopt方法,就是共享缓存,但一般为0走5流程
  5. 根据哈希算法得到一个哈希坐标,放在p12里面,p12 = index
  6. 然后将p13 = buckets[index],得到一个缓存bucket_t
  7. p17 = imp, p9 = sel,将p13的值一个给p17,一个给p9
  8. 开始循环查找,如果说p9就是我们要找的sel(方法),则缓存命中,如果不是则看p9是否为空,如果不为空则执行7,否则就执行MissLabelDynamic也就是我们传过来的__objc_msgSend_uncached方法

源码

/* CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached */

.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant

mov	x15, x16	// 将 Class的值给 x15。
LLookupStart\Function:
	// p1 = SEL, p16 = isa 
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS // 根据上面注意3走下面的宏
	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 //走这里
	ldr	p11, [x16, #CACHE]  // p11 = mask|buckets(将Class平移16字节的值给p11,也就是 p11 = Class的cache = _bucketAndMaybeMask)
    #if CONFIG_USE_PREOPT_CACHES //如果为真机模式走这里
      #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 // 看p11的0号位置不为0的话走LLookupPreopt, 否则继续往下走(在这里我们不看LLookupPreopt方法了)
      #endif
	eor	p12, p1, p1, LSR #7 //异或算法,最终结果放在p12(LSR按位右移)
        //p11, LSR #48 是代表了取 maybeMask的值,然后 and	p12, p12, mask,代表了将算出来的哈希坐标给p12
	and	p12, p12, p11, LSR #48 //得到p12 = 哈希坐标  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
        //根据上面架构判断承接下来,我们仅需知道p10为buckets,p12为哈希坐标(注意这个哈希坐标它可不是最后的位置否则就代表他完全遍历了,如果是完全遍历的话就没必要走下面的4流程了)
        //p12, LSL #(1+PTRSHIFT) 左移4位,得到16进制
        // add	p13, p10, p13 = bucket_t,哈希坐标,得到p13为buckets平移哈希坐标位的值,也就是p13成为了一个bucket_t的值
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

	//p17为imp,p9为sel	            // do {
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
        // 如果说p1就是我们要查的方法则执行2,否则执行3
	cmp	p9, p1				//     if (sel != _cmd) {
	b.ne	3f				//         scan more
	//如果说已经找到方法执行CacheHit,缓存命中    //     } else {
2:	CacheHit \Mode				// hit:    call or return imp
	//看p9是否存在,如果为空则MissLabelDynamic  //     }
3:	cbz	p9, \MissLabelDynamic		//     if (sel == 0) goto Miss;
        //如果说bucket一直在那个边界里面则继续执行1,否则跳出循环
	cmp	p13, p10			// } while (bucket >= buckets)
	b.hs	1b

#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
        //看这里,p13为最终的bucket位置,也就是buckets[mask]
	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 先是上面算出来的哈希坐标index,然后p10是上面的buckets
        // 最终p12就是上面第一次遍历时的bucket_t
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
						// p12 = first probed bucket

        // 看这里,然后,跟1流程差不多,只是现在从最后的位置再往前遍历,也就是完全遍历,但是是不是重复1流程的遍历呢?
						// do {
4:	ldp	p17, p9, [x13], #-BUCKET_SIZE	//     {imp, sel} = *bucket--
        //比较 sel 和 p1的sel,如果命中则继续走2缓存命中
	cmp	p9, p1				//     if (sel == _cmd)
	b.eq	2b				//         goto hit
	//如果 p9(sel)为nil的话走下面流程
        cmp	p9, #0				// } while (sel != 0 &&
        //ccmp 对比两个条件,p12(上面第一次遍历的bucket_t),
        //p13为最终位置bucket_t,然后一直--,如果说p13 > p12的话走下面,否则跳出循环
        //也就是不会重复遍历
	ccmp	p13, p12, #0, ne		//     bucket > first_probed)
	b.hi	4b
// 缓存没找到走objc_msgSend_uncached。下面的条件宏是如果是公用缓存(真机)的情况,才走流程5。
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

objc_msgSend没有缓存命中的情况_objc_msgSend_uncached

我们从上面已经得知objc_msgSend的方法找寻,先是经过缓存查找,如果缓存没找到就要执行_objc_msgSend_uncached。而其实该流程最主要最重要的是lookUpImpOrForward函数。而它不是用汇编写的,是用C++,这个流程就变成了objc_msgSend的慢速流程了

_objc_msgSend_uncached 方法分析

这个方法源码是很少的,就是实行了两个方法而已MethodTableLookupTailCallFunctionPointer

源码

        STATIC_ENTRY __objc_msgSend_uncached
	UNWIND __objc_msgSend_uncached, FrameWithNoSaves

	// 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 方法分析

这个方法是去遍历方法表就是要进入慢速查找流程了,最主要的方法是lookUpImpOrForward,而它的返回值x0,就会继续执行 TailCallFunctionPointer

源码

.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
	bl	_lookUpImpOrForward
        // 下面这个指令如我们所说,x0经常被用作函数返回值    
	// IMP in x0
	mov	x17, x0

	RESTORE_REGS MSGSEND

.endmacro

TailCallFunctionPointer 方法分析

从上面得知我们现在是x17为lookUpImpOrForward的返回值,也就是IMP。本方法有A12处理器情况,也有一个正常情况,拿正常情况源码说明。其实都一样

源码

// $0 为 x17也就是IMP ,br 是跳转指令也就是直接跳转到所找到的函数地址
.macro TailCallFunctionPointer
	// $0 = function pointer value
	br	$0
.endmacro

补充

指令补充

  1. cmp 比较指令,如果说比对成功走下面的条件,否则跳过
  2. b 跳转指令
  3. ldr赋值指令,是将后面的内存地址中指向的内存数据给前一个。
  4. tbnz 比较指令,例如 tbnz p0, #0, 函数,就是比对p0的0号位是否为0。如果不为0走函数,为0就继续走下面指令

共享缓存

例如,苹果每个APP都有一个单独的内存,但是系统的UIKit,Foundation这些个东西总不能每个APP都要加载一下吧,所以,这些个系统的库就会在共享缓存中。而一般情况下,方法的0号位就是标志着是否要去查找共享缓存