这是我参与更文挑战的第10天,活动详情查看: 更文挑战
前言
我们已经探究过方法缓存的插入流程、方法缓存的触发时机、今天继续从汇编层面对方法缓存进行分析;
objc_msgsend、Cache 相关细节
objc_msgsend汇编相关方法细节
.macro GetClassFromIsa_p16 src, needs_auth, auth_address
获取当前对象的class
;.macro ExtractISA
将isa & isa_mask
;
cache 读写过程细节
- 读取过程:
objc_msgsend
->_lookUpImpTryCache
->cachegetIMP
; - 写入过层:
log_fill_cache
->insert()
;
LLDB调用方法特殊细节
LLDB
调用方法的时候,按照我们的猜测,调用1次teacherSay
方法则occpuied=1
,_maybeMask = 3
;但实际上occpuied=1
,_maybeMask = 7
;我们都知道_maybeMask = capacity - 1
;而初始的capacity=4
,这里capacity = 8
肯定是经过insert
之后扩容过的;- 为了查找什么时候执行
insert
方法的,我们在insert
中进行打印输出,最后发现LLDB
调试的时候, 调用1次teacherSay
方法时会额外再进行了2次inser
操作,分别是insert respondsToSelector
和insert class
,所以当insert teacherSay
的时候,发现已经达到扩容条件了,就开始扩容为8了; - 在分析额外
insert
的2个方法的时候,我们发现另外一个点:系统会在reallocate
->allocateBuckets
中,默认把当前bucket
的地址值插入进来作为一个元素,这也是为什么我们比较size
的时候要+1
,以及为什么maybeMask=capacity-1
的原因;
//打印代码
printf("\n%s-------%p---------%p----end\n",(char *)sel,imp,receiver);
LLDB调试源码
(lldb) po [p teacherSay]
respondsToSelector:-------0x100338dd0---------0x101005750----end
class-------0x100338a30---------0x101005750----end
teacherSay-------0x1000037b0---------0x101005750----end
2021-06-27 11:08:32.013613+0800 KCObjcBuild[85513:7316344] -[GCTeacher teacherSay]
retain-------0x1003397a0---------0x1006a0e70----end
release-------0x100339930---------0x1006a0e70----end
dealloc-------0x100339ab0---------0x1006a0e70----end
(lldb)
复制代码
bucket_t::set()细节;
- 当当前缓存可以插入的时候调用
bucket_t::set()
方法,此时在arm
架构下先存imp
后存sel
;在x86
架构下先存sel
后存imp
; arm
真机下先存imp
可能是考虑首地址放imp
减少查找时的开销;
struct bucket_t {
private:
// IMP-first is better for arm64e ptrauth and no worse for arm64.
// SEL-first is better for armv7* and i386 and x86_64.
#if __arm64__
explicit_atomic<uintptr_t> _imp;
explicit_atomic<SEL> _sel;
#else
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
#endif
}
复制代码
二次hash函数cache_next
细节
二次hash
的具体逻辑看下面方法注释;
cache_next(i, m)//再次哈希使用的是当前的位置和容量-1作为参数进行cache_next计算
static inline mask_t cache_next(mask_t i, mask_t mask) {
return i ? i-1 : mask;//如果i不为0,返回i-1,否则返回mask(容量-1);也可以理解为判断发生冲突的位置是不是在buckets的最开头,如果不在最开头就直接前移,如果在最开头就直接跳到容量-1的位置,再依次向前,直到再次遇到一开始的begin位置,此时说明循环了一圈了还没找到空位置插,坏缓存了;
}
复制代码
objc_msgSend 汇编
以下汇编流程仅根据本人理解作为记录,不代表与官方流程完全一致!
_objc_msgSend
主流程汇编源码
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check// p0为消息接受者、这里比较p0和#0
#if SUPPORT_TAGGED_POINTERS //判断是否支持Taggedpointer类型
b.le LNilOrTagged // (MSB tagged pointer looks negative)//如果支持Taggedpointer类型按照LNilOrTagged处理
#else
b.eq LReturnZero // 返回nil:p0和#0相等时,如果不支持Taggedpointer类型返回zero
#endif
ldr p13, [x0] // p13 = isa,[x0]存放的是class
//src, needs_auth, auth_address
//x0 和 p13此时都是isa的地址;
GetClassFromIsa_p16 p13, 1, x0 //从 GetClassFromIsa_p16 中获取clsss:p16 = class
// receiver->class 获取class,去class 中找method cache
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
复制代码
GetClassFromIsa_p16
汇编源码分析
//调用的地方 GetClassFromIsa_p16 p13, 1, x0
.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__ //真机为64位架构
.if \needs_auth == 0 // _cache_getImp takes an authed class already
mov p16, \src//把isa存到P16
.else
// 64-bit packed isa
ExtractISA p16, \src, \auth_address // and $0, $1, #ISA_MASK 这里相当于获取class
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
.endmacro
复制代码
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
汇编源码分析
//CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached,MissLabelConstant
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
//此时x16存放的是class对象,把class对象移动到x15
mov x15, x16 // stash the original isa
//传入LLookupStart的Function参数为_objc_msgSend
LLookupStart\Function:
// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS// arm64位真机下相等
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//KC说的真机走这里
ldr p11, [x16, #CACHE] // p11 = mask|buckets//#CACHE是一个宏定义值为2倍的指针大小即16(2 * __SIZEOF_POINTER__),这行代码作用是将x16即class对象平移16得到cache_t的地址,然后放到p11,即p11 = cache_t的地址
#if CONFIG_USE_PREOPT_CACHES//这里CONFIG_USE_PREOPT_CACHES==1所有走下面一行
#if __has_feature(ptrauth_calls)//这里是arm64e所以走else
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
//p11是cache的地址,这个地址下的第一个成员就是bucketsandmask的首地址,所以这里p11相当于bucketsandmask的地址,在当前的架构(CACHE_MASK_STORAGE_HIGH_16)下cache中定义的时候maskshift = 48位;maskzerobits = 4;bucketsMask = maskshift- maskzerobits-1;bucketsMask的定义在cache中也可以看到;
//取出cache中的bucketsandmask &0x0000fffffffffffe即取前48位,其中47位都为1最后一位为0,意思就是不取最后一位;最终取出的是buckets,然后放进p10中,即p10 = buckets;
and p10, p11, #0x0000fffffffffffe
//tbnz:不为0就跳走,否则走后面的LLookupPreopt\Function
//判断p11与#0做比较,判断p11是否为空;如果p11为空,即bucketsandmask不存在,说明没有缓存就向下一步继续执行,否则走LLookupPreopt\Function;
tbnz p11, #0, LLookupPreopt\Function
#endif
//p0此时为第一个参数:消息接受者receiver;
//p1此时为第二个参数:_cmd,即sel;
//如果bucketsandmask为空则将p1按位右移7位;
eor p12, p1, p1, LSR #7
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
//此时p11=cache_t,cache_t & #0x0000ffffffffffff = buckets()
and p10, p11, #0x0000ffffffffffff // p10 = buckets
//步骤1.p11(buckets中的bucketsandmask)右移48位得到mask的值;
//步骤2.将p1(sel) & mask(步骤1生成的)的结果buckets存入p12;
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
//p12当前方法的buckets;
//PTRSHIFT = 3;
//p12为什么要左移?因为bucket 平移的时候只能平移1/2/3/4个单位,不能直接平移到某个地址;
//步骤1 p12左移#(1+PTRSHIFT)个位置得到的值 + p10得到的值放在p13 即p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT)),p13:当前查找的bucket
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
//ldp 取当前偏移的地址同时存到p17和p9
// bucket里面的东西:imp(p17) sel(p9)
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
//比较我们要查找的sel和p9的值
cmp p9, p1 // if (sel != _cmd) {
//如果不等于,跳转3f
b.ne 3f // scan more
// } else {
2: CacheHit \Mode // hit: call or return imp
// }
//如果p9为空则,查找的对象为nil 则 MissLabelDynamic
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
//p10 为第一个bucket的位置,p13为当前的bucket,用p10和p13对比,如果p13较大就向前平移,然后继续查找
cmp p13, p10 // } while (bucket >= buckets)
b.hs 1b//如果p13
复制代码
objc_msgSend汇编流程总结
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END