前言
前文OC底层原理之类的结构分析分析了cache_t的基本结构,那么缓存是在什么时候读取和插入的呢?本文将对缓存的读取进行探索。
通过前文的分析,我们已经知道,cache_t
是对方法的缓存,那么缓存插入,读取必然和发送消息有关。接下来我们分别对调用方法和performSelector
进行探索:
一、方法调用分析
1.方法调用:
接下来,如下图所示,创建一个XQPerson
的对象,并调用eatFood
方法:
我们通过clang -rewrite-objc main.m -o main.cpp
编译成cpp文件:
可以看到,调用方法实际上是调用objc_msgSend
函数发送消息
2.performSelector
查看performSelector
方法的源码如下:
我们可以看到,performSelector
实际上也是调用objc_msgSend
函数发送消息。
小结:方法调用的实质都是调用objc_msgSend
函数发送消息。
接下来,我们就可以把探索的方向锁定在objc_msgSend
了
二、objc_msgSend汇编分析(本文仅针对arm64真机环境)
准备工作:
下载源码objc-818.2
全局搜索objc_msgSend
,按住command
键点击搜索结果左边的小箭头收起搜索结果,选中objc-msg-arm64.s文件,找到ENTRY _objc_msgSend
,代码如下所示:
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
//将p0(消息接收者)与0进行比较
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
// 判断tagged pointer b.le :判断上面cmp的值是小于等于 执行LNilOrTagged,否则直接往下走
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
//判断是否为空 b.eq :cmp结果 等于 0 执行地址LReturnZero 否则往下
b.eq LReturnZero
#endif
//将[x0]数据写入到寄存器中 即:p13寄存器保存了 对象的isa
ldr p13, [x0] // p13 = isa
//得到calss
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
LGetIsaDone:
//开始查找缓存
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __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
-
cmp p0, #0
判断消息接受者是否为空 -
b.le LNilOrTagged
cmp
的结果是小于等于0则执行LNilOrTagged
判断是否为 tagged pointer 小对象类型 -
b.eq LReturnZero``cmp
结果 等于 0 执行地址LReturnZero 否则往下 -
dr p13, [x0]
将[x0]数据写入到寄存器中 即:p13寄存器保存了 对象的isa -
GetClassFromIsa_p16 p13, 1, x0
isa & ISA_MASK 得到class
.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__
//needs_auth = 1 不满足
.if \needs_auth == 0 // _cache_getImp takes an authed class already
mov p16, \src
.else
// 64-bit packed isa
// src为isa
ExtractISA p16, \src, \auth_address
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
.endmacro
.macro ExtractISA
and $0, $1, #ISA_MASK // $1(isa) & ISA_MASK = $0(p16)即为class
.endmacro
CacheLookup
函数解析
传入参数: NORMAL, _objc_msgSend, __objc_msgSend_uncached
//Mode:NORMAL , Function:_objc_msgSend , MissLabelDynamic : __objc_msgSend_uncached , MissLabelConstant 为空
.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
//模拟器环境或mac
#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
//#define CACHE (2 * __SIZEOF_POINTER__) ,
//x16(class)平移CACHE(16)位得到cache,首地址指向_bucketsAndMaybeMask,唧p11=_bucketsAndMaybeMask
ldr p11, [x16, #CACHE] // p11 = mask|buckets
//64位真机环境
#if CONFIG_USE_PREOPT_CACHES
// A12后处理器
#if __has_feature(ptrauth_calls)
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else //A12以前设备
// p11 & 0x0000fffffffffffe得到buckets
and p10, p11, #0x0000fffffffffffe // p10 = buckets
//判断 p11第0位是否不为0,为0继续下面流程
tbnz p11, #0, LLookupPreopt\Function
#endif
//p12 = p1(_cmd)^(p1(_cmd) >> 7)
eor p12, p1, p1, LSR #7
// p12 得到方法缓存的下标 对应cache_t->insert->cache_hash函数
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
/**
#define PTRSHIFT 3
p10 = buckets, p12 = index(第一次查询的index)
一个bucket_t占用16字节(sel,imp两个指针)
p12(index)左移4(= 1 + PTRSHIFT)位就是index * 16
其实就是buckets首地址加上index个bucket_t内存大小,
找到index位置的bucket,赋值给p13
p13 = p10 + (p12 << 4) = buckets + index * 16,内存平移
p13 = index位置的bucket
*/
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
/**
#define BUCKET_SIZE (2 * __SIZEOF_POINTER__)
x13平移-BUCKET_SIZE,到下个bicket得到 p17(imp),p9(sel)
*/
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
//比较 p9和_cmd
cmp p9, p1 // if (sel != _cmd) {
//不相对执行 3: 开始下次循环
b.ne 3f // scan more
// } else {
//相等,找到缓存
2: CacheHit \Mode // hit: call or return imp
// }
//cbz : 和 0 比较,如果结果为零就转移(只能跳到后面的指令)
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
//b.hs:判断是否无符号小于,满足执行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
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
//得到第一次查询的bucket
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
解析:
mov x15, x16
将isa存储到x15CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
真机环境ldr p11, [x16, #CACHE]
x16
(class)平移CACHE(16)位得到cache,首地址指向_bucketsAndMaybeMask
,即p11
=_bucketsAndMaybeMask
CONFIG_USE_PREOPT_CACHES
64位真机环境__has_feature(ptrauth_calls)
A12后优化,本文不做分析and p10, p11, #0x0000fffffffffffe
p11 & mask 得到buckets
tbnz p11, #0, LLookupPreopt\Function
判断p11
第0
位是否不为0
,为0
继续下面流程eor p12, p1, p1, LSR #7
p12 = p1(_cmd)^(p1(_cmd) >> 7)and p12, p12, p11, LSR #48
p12 得到方法缓存的下标index
对应cache_t->insert-> cache_hash
函数add p13, p10, p12, LSL #(1+PTRSHIFT)
64位#define PTRSHIFT 3
,p10 = buckets
,p12 = index
(第一次查询的index
),一个bucket_t
占用16
字节(sel
,imp
两个指针),p12
(index)左移4
(= 1 + PTRSHIFT)位就是index
* 16,其实就是buckets
首地址加上index
个bucket_t
内存大小,找到index
位置的bucket
,赋值给p13
即:p13 = p10 + (p12 << 4) = buckets + index * 16
。通过内存平移内存平移p13
=index
位置的bucket
1:ldp p17, p9, [x13], #-BUCKET_SIZE
64位#define BUCKET_SIZE (2 * __SIZEOF_POINTER__)
x13
平移-BUCKET_SIZE
,到下个bicket
得到p17(imp)
,p9(sel)
cmp p9, p1
,判断p9
和p1
是否相等,相等执行2:2: CacheHit \Mode
,不相等执行3: cbz p9, \MissLabelDynamic
2: CacheHit \Mode
找到缓存,执行CacheHit``Mode
=Noamal
3: cbz p9, \MissLabelDynamic
p9(sel)
和 0 比较,如果结果为零就执行MissLabelDynamic(传的参数 __objc_msgSend_uncached)
,b.hs 1b
:判断上面比较的结果是否无符号小于,为真则执行,cmp p13, p10
比较,b.hs 1b
,如果满足p13 >= p10
继续执行循环1:ldp p17, p9, [x13], #-BUCKET_SIZE
- 如果循环执行完成依然没有查询到缓存则继续下面的流程,按架构进入
CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
得到最后一个bucketadd p12, p10, p12, LSL #(1+PTRSHIFT)
得到第一次查询的bucket4: ldp p17, p9, [x13], #-BUCKET_SIZE
依次遍历,并将imp
存储到p17
,sel
存储到p9
。cmp p9, p1
比较,p9(sel)
与p1(_cmd)
,b.eq 2b
比较相等,执行2b(查找到缓存)
,cmp p9, #0
判断p9(sel)
是否为空,ccmp p13, p12, #0, ne
比较p13(bucket)
与p12(first_probed)
,如果p13
,p12
都 > 0p13
>p12
执行4b
继续循环。- 查询不到执行
MissLabelDynamic(__objc_msgSend_uncached)
部分架构判断图解:
CacheHit
解析:
传入参数 NORMAL
// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
// x17=imp,x10 = bucket,x1 = _cmd x16 = class
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 = NORMAL
执行TailCallCachedImp x17, x10, x1, x16
,参数分别为x17=imp,x10 = bucket,x1 = _cmd x16 = class
TailCallCachedImp
解析:
.macro TailCallCachedImp
// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
// imp = imp ^ cls ,对应 `bucket_t`的 `encodeImp`函数`(uintptr_t)newImp ^ (uintptr_t)cls;`
eor $0, $0, $3
// 跳转到imp 执行
br $0
.endmacro
__objc_msgSend_uncached
解析:
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
解析:
.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
// 进入lookUpImpOrForward函数 参数 x0 receiver(self) x1 _cmd , x2 class, x3 = 3
bl _lookUpImpOrForward
// IMP in x0
mov x17, x0
RESTORE_REGS MSGSEND
.endmacro
解析:跳转到lookUpImpOrForward
函数,传入receiver(self)
,_cmd
, class
及 3
, lookUpImpOrForward
函数后续发文分析。
三、缓存查找流程图:
总结:
方法的快速查找流程是通过调用 objc_msgSend
,receiver
取出 class
,遍历 cache
缓存的 bucket
,依次取出bucket
的sel
与 _cmd
进行比较并判断执行的过程。