OC底层探究之从cache到objc_msgSend

1、cache插入流程分析

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

接下来咱们就开始探索方法是什么时候插入缓存的!java

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

image-20210627173608622

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

image-20210627173749055

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

_objc_msgSend_uncached -> lookUpImpOrForward -> log_and_fill_cache -> insertapi

这里能够看到对象调用方法的时候最早调用的是objc_msgSend方法!缓存

objc_msgSend方法就涉及到了runtimemarkdown

2、runtime的运行时理解

一、静态编程和动态编程

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

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

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

二、编译时

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

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

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

因此有时⼀些⼈说编译时还分配内存啥的确定是错误的说法

三、运行时

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

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

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

四、runtime发起方式

一、OC的方法。

二、NSObject的方法。

三、objc动态库的api。

能够经过一个图来表示层级:

runtime层级

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

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

image-20210629114546717

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

六、经过底层分析

接下来咱们经过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 + 参数))

七、底层代码调用

那咱们是否能够直接在代码中进行调用呢?

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

image-20210629140657255

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

而后咱们尝试直接在代码中调用:

image-20210629140948759

发现和咱们正常使用代码调用方法是如出一辙的!

八、NSObject方法调用

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

进入到NSObject.h中就能够看到相关方法了:

image-20210629151359255

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

image-20210629151650360

和咱们正常调用是同样的!

3、查看objc_msgSend源码

一、汇编调试

先打开汇编image-20210629173430049

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

image-20210629173558884

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

image-20210629173828894

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

二、查看源码

打开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++写的,这是为何呢?

由于汇编,并且具备动态化

4、汇编源码分析

一、汇编的常见指令

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)

二、开始分析

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首地址

三、获取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

四、查找缓存-CacheLookup

4.一、总览

获取了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.二、获取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.三、开始查找

继续往下走:

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开始值(begin)

拿到哈希下标后:

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进行内存平移取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

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

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

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

为何p9(sel)为空就miss呢?

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

二、真机状况下,存缓存的时候若是hash冲突了,则:

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

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

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

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的过程!

4.五、继续查找

若是p13(bucket)p10(buckets)小,则跳出循环,继续往下走:

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

只用看CACHE_MASK_STORAGE_HIGH_16

add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
						// p13 = buckets + (mask << 1+PTRSHIFT)
						// see comment about maskZeroBits
复制代码

这里maskbuckets开辟的内存-1,因此这里是把buckets右移mask个内存!

因此p13buckets的最后一个bucket

接着:

add	p12, p10, p12, LSL #(1+PTRSHIFT)
						// p12 = first probed bucket
复制代码

这里是p12(begin)左移4位,即经过hash获得的begin * 16,而后再把p10(buckets)右移p12大小的内存。

因此p12为以前hash获得的begin处的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
复制代码

这里上面的循环相似:

一、先赋值impselp17p9,而后bucket--

二、比较p9(sel)传入的sel比较,相同则进入缓存命中CacheHit

三、比较p9(sel)是否不为0,并且bukcet是否大于p12(begin处的bucket)

四、知足则跳回4(循环的开始处)。

4.六、没有找到

若是p9(sel)0,或者bukcet小于p12(begin处的bucket),则继续:

LLookupEnd\Function:
LLookupRecover\Function:
	b	\MissLabelDynamic
复制代码

这里就是没有找到缓存,进入MissLabelDynamic(__objc_msgSend_uncached)

5、总结

objc_msgSend