OC底层探究之从cache到objc_msgSend

一、cache插入流程分析

《OC底层探究之类的cache_t分析》中探索了方法是如何存入类中的缓存的!但是方法是何时存入类的缓存的呢?

接下来我们就开始探索方法是何时插入缓存的!

先打开objc源码,在调用方法处打上断点

image-20210627173608622

等断住后,在insert方法中打上断点,断住后在lldb中使用bt查看堆栈

image-20210627173749055

在堆栈中可以看到从对象调用方法进入到insert方法的整个流程:

_objc_msgSend_uncached -> lookUpImpOrForward -> log_and_fill_cache -> insert

这里可以看到对象调用方法的时候最先调用的是objc_msgSend方法!

objc_msgSend方法就涉及到了runtime

二、runtime的运行时理解

1、静态编程和动态编程

首先我们要先知道编程语言有静态和动态之分。

所谓静态语言,就是在程序运行前决定了所有的类型判断,类的所有成员、方法在编译阶段(即编译时)就确定好了内存地址。也就意味着所有类对象只能访问属于自己的成员变量和方法,否则编译器直接报错。比较常见的静态的语言如:java,c++,c等等。

而动态语言,恰恰相反,类型的判断、类的成员变量、方法的内存地址都是在程序的运行阶段(即运行时)才最终确定,并且还能动态的添加成员变量和方法。也就意味着你调用一个不存在的方法时,编译也能通过,甚至一个对象它是什么类型并不是表面我们所看到的那样,只有运行之后才能决定其真正的类型。相比于静态语言,动态语言具有较高的灵活性和可订阅性。而oc,正是一门动态语言。

2、编译时

编译时顾名思义就是正在编译的时候。那啥叫编译呢?就是编译器帮你把源代码翻译成机器能识别的代码。(当然只是⼀般意义上这么说,实际上可能只是翻译成某个中间状态的语⾔。)

那么编译时就是简单的作⼀些翻译⼯作,⽐如检查⽼兄你有没有粗⼼写错啥关键字了啊、词法分析、语法分析之类的过程。 就像个⽼师检查学⽣的作⽂中有没有错别字和病句⼀样 。如果发现啥错误编译器就告诉你。

如果你⽤微软的VS的话,点下build.那就开始编译,如果下⾯有errors或者warning信息,那都是编译器检查出来的。所谓这时的错误就叫编译时错误,这个过程中做的啥类型检查也就叫编译时类型检查,或静态类型检查(所谓静态嘛就是没把真把代码放内存中运⾏起来,⽽只是把代码当作⽂本来扫描下)。

所以有时⼀些⼈说编译时还分配内存啥的肯定是错误的说法

3、运行时

运⾏时,就是代码跑起来了,被装载到内存中去了。

你的代码保存在磁盘上没装⼊内存之前是个死家伙,只有跑到内存中才变成活的。⽽运⾏时类型检查就与前⾯讲的编译时类型检查(或者静态类型检查)不⼀样.不是简单的扫描代码。⽽是在内存中做些操作,做些判断。

关于运行时更多详细的内容可以去官方文档上进行查阅。(Objective-C 运行时编程指南

4、runtime发起方式

1、OC的方法。

2、NSObject的方法。

3、objc动态库的api。

可以通过一个图来表示层级:

runtime层级

5、运行时和编译时的区别

创建一个对象,有两个方法,只实现一个方法,运行一下:

image-20210629114546717

就会发现,编译是成功的,但是一运行就报错了,这就是编译时运行时的区别!

6、通过底层分析

接下来我们通过clang的还原,看看OC代码在底层的实现,找到main函数:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        HPerson * p = ((HPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("HPerson"), sel_registerName("alloc"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("saySix"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("sayHello"));
    }
    return 0;
}
复制代码

发现编译后上层的代码都会得到一个解释

调用方法的过程就是调用objc_msgSend函数,即消息发送!

通过底层代码我们可以发现objc_msgSend函数有2个参数:

一个是(id)objc_getClass("HPerson")或者(id)p,即消息的接受者!

一个是sel_registerName("xxx")sel

现在调用的方法都是没有带参数的,如果是带有参数的呢?

我们加上参数
image-20210629120214511

clang一下:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        HPerson * p = ((HPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("HPerson"), sel_registerName("alloc"));
        ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)p, sel_registerName("saySomething:"), (NSString *)&__NSConstantStringImpl__var_folders_1h_55lzq4fd39b0mz94wmqthqf860mpgy_T_main_189844_mi_2);
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("saySix"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("sayHello"));
    }
    return 0;
}
复制代码

可以看到多了一个NSString参数!

所以可以得出消息发送方式

objc_msgSend (消息接受者, 消息主体(sel + 参数))

7、底层代码调用

那我们是否可以直接在代码中进行调用呢?

在调用之前先在Build Setting中将Enable Strict Checking of objc_msgSend Calls改为NO:

image-20210629140657255

意思是对调用objc_msgSend函数的要求放宽,由我们来决定参数!

然后我们尝试直接在代码中调用:

image-20210629140948759

发现和我们正常使用代码调用方法是一模一样的!

8、NSObject方法调用

那么NSObject有是怎么调用的呢?

进入到NSObject.h中就可以看到相关方法了:

image-20210629151359255

很明显performSelector方法有关,我们来尝试一下:

image-20210629151650360

和我们正常调用是一样的!

三、查看objc_msgSend源码

1、汇编调试

先打开汇编
image-20210629173430049

然后在方法前打上断点,运行:

image-20210629173558884

objc_msgSend处打上断点,断住后,按住ctrl点击step into

image-20210629173828894

即可发现objc_msgSend是来自objc底层源码!

2、查看源码

打开objc源码,搜索objc_msgSend

image-20210629174130568

因为是objc_msgSend的底层是汇编写的,所以我们直接看.s文件!

因为我们用的最多还是真机,所以我们看arm64的:

image-20210629174445925

找到ENTRY _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是用汇编写的,而我们之前的源码都是用c或者c++写的,这是为什么呢?

因为汇编

四、汇编源码分析

1、汇编的常见指令

b 指令

  • bl 跳转到标号出执行
  • b.le 判断上面cmp的值是小于等于 执行标号,否则直接往下走
  • b.ge 大于等于 执行地址 否则往下
  • b.lt 判断上面camp的值是 小于 执行后面的地址中的方法 否则直接往下走
  • b.gt 大于 执行地址 否则往下
  • b.eq 等于 执行地址 否则往下
  • b.hi 比较结果是无符号大于,执行地址中的方法,否则不跳转
  • b.hs 指令是判断是否无符号小于
  • b.ls 指令是判断是否无符号大于
  • b.lo 指令是判断是否无符号大于等于

ret 返回

  • mov x0,#0x10 -> x0 = 0x10
  • str w10 ,[sp] 将w10寄存器的值存到 sp栈空间内存
  • stp x0,x1,[sp.#0x10]* : x0、x1 的值存入 sp + 0x10
  • orr x0,wzr,#0x1 : x0 = wzr | 0x1
  • stur w10 ,[sp] 将w10寄存器的值存到 sp栈空间内存
  • ldr w10 ,[sp] w10 = sp栈内存中的值
  • ldp x0,x1,[sp] x0、x1 = sp栈内存中的值

adrp 通过基地址 + 偏移 获得一个字符串(全局变量)

  • cbz 比较,为零则跳转;
  • cbnz: 比较,为非零则跳转。
  • cmp: 比较功能 例如 : cmp OPR1 , OPR2. = (OPR1)-(OPR2)

2、开始分析

cmp	p0, #0			// nil check and tagged pointer check
复制代码

搜索p0

#if __LP64__
// true arm64

#define SUPPORT_TAGGED_POINTERS 1
#define PTR .quad
#define PTRSIZE 8
#define PTRSHIFT 3  // 1<<PTRSHIFT == PTRSIZE
// "p" registers are pointer-sized
#define UXTP UXTX
#define p0  x0
#define p1  x1
#define p2  x2
#define p3  x3
#define p4  x4
#define p5  x5
#define p6  x6
#define p7  x7
#define p8  x8
#define p9  x9
#define p10 x10
#define p11 x11
#define p12 x12
#define p13 x13
#define p14 x14
#define p15 x15
#define p16 x16
#define p17 x17

// true arm64
#else
// arm64_32

#define SUPPORT_TAGGED_POINTERS 0
#define PTR .long
#define PTRSIZE 4
#define PTRSHIFT 2  // 1<<PTRSHIFT == PTRSIZE
// "p" registers are pointer-sized
#define UXTP UXTW
#define p0  w0
#define p1  w1
#define p2  w2
#define p3  w3
#define p4  w4
#define p5  w5
#define p6  w6
#define p7  w7
#define p8  w8
#define p9  w9
#define p10 w10
#define p11 w11
#define p12 w12
#define p13 w13
#define p14 w14
#define p15 w15
#define p16 w16
#define p17 w17

// arm64_32
#endif
复制代码

可以发现p0,是指寄存器x0!即我们传入的第一个参数p

这条指令的意思就让p00进行对比,判断p0是否为

即判断消息接收者是否存在,如果不存在则:

#if SUPPORT_TAGGED_POINTERS
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative)
#else
	b.eq	LReturnZero
#endif
复制代码

SUPPORT_TAGGED_POINTERS1时,进入LNilOrTagged

#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
	b.eq	LReturnZero		// nil check
	GetTaggedClass
	b	LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
复制代码

SUPPORT_TAGGED_POINTERS0时类似,最后都是执行LReturnZero

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
复制代码

即把寄存器清空,然后结束_objc_msgSend

正常情况当然是继续往下走:

ldr	p13, [x0]		// p13 = isa
复制代码

x0赋值给p13x0即是消息接受者,即传入的第一个参数p

注释表明是将isa赋值给p13,为什么是isa呢?

因为isap首地址

3、获取class-GetClassFromIsa_p16

继续:

GetClassFromIsa_p16 p13, 1, x0	// p16 = class
复制代码

看注释表明是将class赋值给了p16

开始探索GetClassFromIsa_p16

调用GetClassFromIsa_p16,并将p131x0传入。

进入GetClassFromIsa_p16

/********************************************************************
 * GetClassFromIsa_p16 src, needs_auth, auth_address
 * src is a raw isa field. Sets p16 to the corresponding class pointer.
 * The raw isa might be an indexed isa to be decoded, or a
 * packed isa that needs to be masked.
 *
 * On exit:
 *   src is unchanged
 *   p16 is a class pointer
 *   x10 is clobbered
 ********************************************************************/
 
.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__
.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
复制代码

.macro表示这是一个宏定义!

先看SUPPORT_INDEXED_ISA

// Define SUPPORT_INDEXED_ISA=1 on platforms that store the class in the isa 
// field as an index into a class table.
// Note, keep this in sync with any .s files which also define it.
// Be sure to edit objc-abi.h as well.
#if __ARM_ARCH_7K__ >= 2  ||  (__arm64__ && !__LP64__)
#   define SUPPORT_INDEXED_ISA 1
#else
#   define SUPPORT_INDEXED_ISA 0
#endif
复制代码

现在基本都是64位,所以我们主要看SUPPORT_INDEXED_ISA0的情况,以及为__LP64__的情况:

.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
复制代码

needs_auth为第二个参数,即1!

所以走的是ExtractISA,传入p16p13(isa)x0

.macro ExtractISA
	and    $0, $1, #ISA_MASK
.endmacro
复制代码

这里是让$1#ISA_MASK相加,然后赋值给了$0

这里和我们之前看的源码很像!即isa&ISA_MASK,得到class

所以这一步就将class赋值给了p16

为什么要把class取出来呢?因为cache是在class里面,取出class后就要准备插入缓存了!

然后结束GetClassFromIsa_p16

4、查找缓存-CacheLookup

4.1、总览

获取了class后:

LGetIsaDone:
	// calls imp or objc_msgSend_uncached
	CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
复制代码

接着就进入到了CacheLookup,即查找缓存:

/********************************************************************
 *
 * CacheLookup NORMAL|GETIMP|LOOKUP <function> MissLabelDynamic MissLabelConstant
 *
 * MissLabelConstant is only used for the GETIMP variant.
 *
 * Locate the implementation for a selector in a class method cache.
 *
 * When this is used in a function that doesn't hold the runtime lock,
 * this represents the critical section that may access dead memory.
 * If the kernel causes one of these functions to go down the recovery
 * path, we pretend the lookup failed by jumping the JumpMiss branch.
 *
 * Takes:
 *	 x1 = selector
 *	 x16 = class to be searched
 *
 * Kills:
 * 	 x9,x10,x11,x12,x13,x15,x17
 *
 * Untouched:
 * 	 x14
 *
 * On exit: (found) calls or returns IMP
 *                  with x16 = class, x17 = IMP
 *                  In LOOKUP mode, the two low bits are set to 0x3
 *                  if we hit a constant cache (used in objc_trace)
 *          (not found) jumps to LCacheMiss
 *                  with x15 = class
 *                  For constant caches in LOOKUP mode, the low bit
 *                  of x16 is set to 0x1 to indicate we had to fallback.
 *          In addition, when LCacheMiss is __objc_msgSend_uncached or
 *          __objc_msgLookup_uncached, 0x2 will be set in x16
 *          to remember we took the slowpath.
 *          So the two low bits of x16 on exit mean:
 *            0: dynamic hit
 *            1: fallback to the parent class, when there is a preoptimized cache
 *            2: slowpath
 *            3: preoptimized cache hit
 *
 ********************************************************************/

#define NORMAL 0
#define GETIMP 1
#define LOOKUP 2

.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
	//

	mov	x15, x16			// stash the original isa
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
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	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
#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
#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

	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

	// 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
	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
	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

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
复制代码

4.2、获取buckets

首先看传入的参数:NORMAL, _objc_msgSend, __objc_msgSend_uncached,一共3个参数。

分别对应Mode, Function, MissLabelDynamic, MissLabelConstant

但是这个函数需要传入4个参数,说明最后一个为默认值

然后跟着继续往下走:

mov	x15, x16			// stash the original isa
复制代码

这里是将x16class赋值给了x15

继续:

LLookupStart\Function:
	// p1 = SEL, p16 = isa
复制代码

Function为传入的_objc_msgSend,即开始_objc_msgSend

接着就来到了一个判断

#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	//...
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
	//...
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	//...
#else
#error Unsupported cache mask storage for ARM64.
#endif
复制代码

我们先看看CACHE_MASK_STORAGE

#if defined(__arm64__) && __LP64__ 
#if TARGET_OS_OSX || TARGET_OS_SIMULATOR
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_HIGH_16 //真机64位
#endif
#elif defined(__arm64__) && !__LP64__
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_LOW_4
#else
#define CACHE_MASK_STORAGE CACHE_MASK_STORAGE_OUTLINED
#endif
复制代码

因为我们主要看真机64位的模式,所以只需要看CACHE_MASK_STORAGE_HIGH_16即可:

	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
#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
复制代码

先看ldr,即存储值,这里是先把x16加上#CACHE,然后在赋值p11

找一下#define CACHE

#define CACHE            (2 * __SIZEOF_POINTER__)
复制代码

__SIZEOF_POINTER__是指针的大小,即CACHE16

所以是x16平移16字节,即class平移16字节得到cache!再赋值给p11,即p11就是cache的首地址(即_bucketsAndMaybeMask)!

接着看CONFIG_USE_PREOPT_CACHES

#if defined(__arm64__) && TARGET_OS_IOS && !TARGET_OS_SIMULATOR && !TARGET_OS_MACCATALYST
#define CONFIG_USE_PREOPT_CACHES 1
#else
#define CONFIG_USE_PREOPT_CACHES 0
#endif
复制代码

真机时为1!即只用看这一段:

#if __has_feature(ptrauth_calls)
	tbnz	p11, #0, LLookupPreopt\Function
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
	tbnz	p11, #0, LLookupPreopt\Function
复制代码

__has_feature:此函数的功能是判断编译器是否支持某个功能。

ptrauth_calls:指针身份验证,针对 arm64e 架构;使用 Apple A12 或更高版本 A 系列处理器的设备(如 iPhone XSiPhone XS MaxiPhone XR 或更新的设备)支持 arm64e 架构。

我们看大多数情况,即A12以下的真机,即else部分!

先是把p11&0x0000fffffffffffe再赋值给了p10!那么p10是什么呢?

回顾获取buckets的方法:

struct bucket_t *cache_t::buckets() const
{
    uintptr_t addr = _bucketsAndMaybeMask.load(memory_order_relaxed);
    return (bucket_t *)(addr & bucketsMask);
}
复制代码

这里p11cache的首地址,即_bucketsAndMaybeMask,而0x0000fffffffffffe为掩码,所以p10buckets

拿到buckets后:

tbnz	p11, #0, LLookupPreopt\Function
复制代码

tbnz:第0位不为0则发生跳转。

一般情况buckets都是为0的。

4.3、开始查找

继续往下走:

eor	p12, p1, p1, LSR #7
and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
复制代码

LSR:按位右移。

这里和之前insert方法的获取hash是一样的:

static inline mask_t cache_hash(SEL sel, mask_t mask) 
{
    uintptr_t value = (uintptr_t)sel;
#if CONFIG_USE_PREOPT_CACHES
    value ^= value >> 7;
#endif
    return (mask_t)(value & mask);
}
复制代码

所以这里是重新hash,获取新的下标,即p12新的下标值

拿到哈希下标后:

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

LSL:按位左移。

然后找PTRSHIFT

#if __LP64__
#define PTRSHIFT 3  // 1<<PTRSHIFT == PTRSIZE
#else
#define PTRSHIFT 2  // 1<<PTRSHIFT == PTRSIZE
#endif
复制代码

所以PTRSHIFT3

这里p12hash下标,左移4位则p12的值乘以16即为内存大小!

所以这里是p10这个buckets内存平移!即在buckets内重新查找新的bucket

所以p13是当前查找的bucket

接着:

						// 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
复制代码

ldp x0,x1,[sp] :x0、x1 = sp栈内存中的值。

cbz :比较,为零则跳转。

分析:

1:将当前bucketimpsel分别赋值给p17p9,然后x13-16,即x13为前一个bucket,接着用p9(sel)我们传进来的方法进行比较,如果不一样则跳到3,如果一样则跳到2

2、找到了我们传进来的方法,即缓存命中CacheHit

3、如果p9(sel)为空,则进入MissLabelDynamicCacheLookup函数传入的第三个参数__objc_msgSend_uncached,如果不为空,判断当前x13这个bucket地址是否大于等于p10这个buckets首地址,大于等于则跳转到1,否则接着往下走!

3步就是一个do...while循环!

为什么p9(sel)为空就miss呢?

1、首先取缓存和存缓存的hash算法是一样的。

2、真机情况下,存缓存的时候如果hash冲突了,则:

static inline mask_t cache_next(mask_t i, mask_t mask) {
    return i ? i-1 : mask;
}
复制代码

说明真机是按位存储缓存的,所以发现为空说明缓存已经找完了,而且没有找到,则miss!

同理,当bucket的地址小于buckets的地址时,也说明缓存已找完了,即跳出循环!

4.4、CacheHit

当找到了我们传入的方法后,就会进入到CacheHit

// 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
复制代码

$0CacheLookup函数传入的第一个值,即为NORMAL

所以只用看:

TailCallCachedImp x17, x10, x1, x16	// authenticate and call imp
复制代码

接着进入TailCallCachedImp函数:

#if __has_feature(ptrauth_calls)
// JOP
.macro TailCallCachedImp
	// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
	eor	$1, $1, $2	// mix SEL into ptrauth modifier
	eor	$1, $1, $3  // mix isa into ptrauth modifier
	brab	$0, $1
.endmacro
#else
// not JOP
.macro TailCallCachedImp
	// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
	eor	$0, $0, $3
	br	$0
.endmacro
#endif
复制代码

我们看A12以下的情况:

.macro TailCallCachedImp
	// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
	eor	$0, $0, $3
	br	$0
.endmacro
复制代码

eor:按位异或。

我们传入了x17(imp), x10(buckets), x1(传入的sel), x16(isa)

然后把$0(imp)$3(isa即类)进行异或,再赋值给$0

这里为什么要异或呢?

在我们插入缓存的时候,进行了编码

// Sign newImp, with &_imp, newSel, and cls as modifiers.
    uintptr_t encodeImp(UNUSED_WITHOUT_PTRAUTH bucket_t *base, IMP newImp, UNUSED_WITHOUT_PTRAUTH SEL newSel, Class cls) const {
        if (!newImp) return 0;
#if CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_PTRAUTH
        return (uintptr_t)
            ptrauth_auth_and_resign(newImp,
                                    ptrauth_key_function_pointer, 0,
                                    ptrauth_key_process_dependent_code,
                                    modifierForSEL(base, newSel, cls));
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_ISA_XOR
        return (uintptr_t)newImp ^ (uintptr_t)cls;
#elif CACHE_IMP_ENCODING == CACHE_IMP_ENCODING_NONE
        return (uintptr_t)newImp;
#else
#error Unknown method cache IMP encoding.
#endif
    }
复制代码

所以这里是解码获得imp

最后跳转imp

到这里就是objc_msgSend通过sel查找imp的过程!

五、总结

objc_msgSend

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享