一、cache插入流程分析
在《OC底层探究之类的cache_t分析》中探索了方法是如何存入类中的缓存的!但是方法是何时存入类的缓存的呢?
接下来我们就开始探索方法是何时插入缓存的!
先打开objc源码
,在调用方法
处打上断点
:
等断住后,在insert
方法中打上断点
,断住后在lldb
中使用bt
查看堆栈
:
在堆栈中可以看到从对象调用方法
到进入到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。
可以通过一个图来表示层级:
5、运行时和编译时的区别
创建一个对象,有两个方法,只实现一个方法,运行一下:
就会发现,编译
是成功的,但是一运行
就报错了,这就是编译时
和运行时
的区别!
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
!
现在调用的方法都是没有带参数的,如果是带有参数的呢?
我们加上参数
:
再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:
意思是对调用objc_msgSend
函数的要求放宽,由我们来决定参数!
然后我们尝试直接在代码中调用:
发现和我们正常使用代码调用方法是一模一样的!
8、NSObject方法调用
那么NSObject
有是怎么调用的呢?
进入到NSObject.h
中就可以看到相关方法了:
很明显performSelector
和方法
有关,我们来尝试一下:
和我们正常调用是一样的!
三、查看objc_msgSend源码
1、汇编调试
先打开汇编
:
然后在方法前打上断点
,运行:
在objc_msgSend
处打上断点,断住后,按住ctrl
点击step into
:
即可发现objc_msgSend
是来自objc
底层源码!
2、查看源码
打开objc
源码,搜索objc_msgSend
:
因为是objc_msgSend
的底层是汇编写的,所以我们直接看.s
文件!
因为我们用的最多还是真机,所以我们看arm64的:
找到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 + 0x10orr x0,wzr,#0x1
: x0 = wzr | 0x1stur 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
。
这条指令的意思就让p0
和0
进行对比,判断p0
是否为空
!
即判断消息接收者
是否存在,如果不存在则:
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
复制代码
当SUPPORT_TAGGED_POINTERS
为1
时,进入LNilOrTagged
:
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
复制代码
和SUPPORT_TAGGED_POINTERS
为0
时类似,最后都是执行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
赋值给p13
,x0
即是消息接受者
,即传入的第一个参数p
。
注释表明是将isa
赋值给p13
,为什么是isa
呢?
因为isa
即p
的首地址
!
3、获取class-GetClassFromIsa_p16
继续:
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
复制代码
看注释表明是将class
赋值给了p16
!
开始探索GetClassFromIsa_p16
!
调用GetClassFromIsa_p16
,并将p13
,1
,x0
传入。
进入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_ISA
为0
的情况,以及为__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
,传入p16
,p13(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
复制代码
这里是将x16
即class
赋值给了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__
是指针的大小,即CACHE
为16
!
所以是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 XS
、iPhone XS Max
和 iPhone 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);
}
复制代码
这里p11
是cache
的首地址,即_bucketsAndMaybeMask
,而0x0000fffffffffffe
为掩码,所以p10
为buckets
!
拿到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
复制代码
所以PTRSHIFT
为3
!
这里p12
为hash
下标,左移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:将当前bucket
的imp
和sel
分别赋值给p17
和p9
,然后x13-16
,即x13
为前一个bucket
,接着用p9(sel)
和我们传进来的方法
进行比较,如果不一样则跳到3
,如果一样则跳到2
。
2、找到了我们传进来的方法,即缓存命中CacheHit
。
3、如果p9(sel)
为空,则进入MissLabelDynamic
即CacheLookup
函数传入的第三个参数__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
复制代码
$0
为CacheLookup
函数传入的第一个值,即为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
的过程!