前言
在上一篇iOS底层原理-cache_t分析对cache
的结构和方法的存储做了分析,通过LLDB
和示例调试
进行了验证,这些是通过查看源码找到相应的insert()
方法,但这样就有个疑问,cache
是什么时候插入的?
cache是什么时候写入的?
LLDB调试
我们在对象调起方法时打个断点,当断住时在cache_t::insert
方法再打个断点(确定为当前对象调用方法走的insert),进到insert
方法里,输入bt
打印当前堆栈信息。
可以看到在调起insert
之前调用了log_and_fill_cache
方法,这时我们再搜索一下,可以找到在lookUpImpOrForward
方法里调用了,跟上面截图的调用结果一致。
Cache的定义
打开cache
的源码可以看到下面的一段注释
/*
* Cache readers (PC-checked by collecting_in_critical())
* objc_msgSend*
* cache_getImp
*
* Cache readers/writers (hold cacheUpdateLock during access; not PC-checked)
* cache_t::copyCacheNolock (caller must hold the lock)
* cache_t::eraseNolock (caller must hold the lock)
* cache_t::collectNolock (caller must hold the lock)
* cache_t::insert (acquires lock)
* cache_t::destroy (acquires lock)
*/
从注释可以看到在cache_t::insert
之前有cache_getImp
和objc_msgSend*
,下面着重了解一下objc_msgSend
。
Runtime
在分析objc_msgSend
之前,先来了解一下Runtime
的概念
The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language, and as such is linked to by all Objective-C apps. Objective-C runtime library support functions are implemented in the shared library found at /usr/lib/libobjc.A.dylib.
Runtime的发起方式
- ObjectIve-C层面调起相关方法
- NSObject接口
- objc底层API
Clang编译
我们通过Clang对main.m进行编译,看编译后的Objective-C是如何调用方法的?
main.m
LGPerson *p = [LGPerson alloc];
[p saySomething];
// clang对main.m进行编译
clang -rewrite-objc main.m -o main.cpp
编译后得到下面的调用函数
LGPerson *p = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("saySomething"));
根据 clang
编译结果我们得知方法的本质是发送消息,底层实现是 objc_msgSend(消息接收者, 消息主体[sel+参数])
汇编调试
在[p saySomething]的调用方法上打个断点,然后以汇编的模式进行查看(Debug ~> Debug Workflow ~> Always show Disassembly
),然后按住control键,点击step into,进入objce_msgSend
。
在对象调用方法时通过汇编断点来看一下发送消息的流程,可以找到 objc_msgSend
定位到 libobjc.dylib
文件,因此我们在源码全局搜索 objc_msgSend
,汇编是以.s文件结尾,我们找到 objc-msg-arm64.s
(以arm64架构),这就是Object-C中著名的objc_msgSend
流程,下面我们通过汇编的方式了解一下消息是怎么发送的?
objc_msgSend分析
通过搜索我们可以找到objc_msgSend
标识为ENTRY
入口的地方继续分析。
常用汇编指令
cmp
: 比较指令ldr
: 把后面的值存入前面的地址中mov
: 寄存器加载数据,既能用于寄存器间的传输,也能用于加载立即数eor
: 异或操作lsr
: 逻辑右移lsl
: 逻辑左移adrp
: 通过基地址 + 偏移 获得一个字符串(全局变量)ldp
x0,x1,[sp] x0、x1 = sp栈内存中的值
以上是本篇用到的基本汇编指令,详情请参考Cooci的这篇文章。
汇编源码
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // p0 消息接收者 判断receiver是否为空
#if SUPPORT_TAGGED_POINTERS // 判断是否为tagged pointer类型
b.le LNilOrTagged // tagged pointer类型
#else
b.eq LReturnZero // 非tagged pointer类型 LReturnZero返回nil
#endif
ldr p13, [x0] // 把x0的内存地址存入p13, x0:消息接收者,地址就是它的首地址isa, p13 = isa
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
上面汇编代码解析:
- 判断对象是否为空
- 判断是否为
tagged pointer
类型,是走LNilOrTagged
,不是走LReturnZero
(返回nil) x0
的首地址存入p13
,p13 = isa
对于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,根据真机运行这里为0
// 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 // 根据上面的传值needs_auth = 1
mov p16, \src
.else
// 64-bit packed isa
//
ExtractISA p16, \src, \auth_address
.endif
#else
// 32-bit raw isa
mov p16, \src
#endif
.endmacro
根据SUPPORT_INDEXED_ISA = 0
, needs_auth = 1
,所以定位到ExtractISA p16, \src, \auth_address
这行代码,再查一下ExtractISA
的定义
#if __has_feature(ptrauth_calls)
.macro ExtractISA
and $0, $1, #ISA_MASK
.endmacro
#else
// ExtractISA p16, \src = p13(isa), \auth_address(isa)
.macro ExtractISA
and $0, $1, #ISA_MASK // $0 = $1 & ISA_MASK
.endmacro
根据上面的定义,我们得出ExtractISA
的操作是$0 = $1 & ISA_MASK
,也就是p16 = isa & ISA_MASK
,所以p16 = class
。接下来就是LGetIsaDone
,然后走下面的代码
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
再搜索一下CacheLookup
的定义
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
mov x15, x16 // 把p16的地址移动到x15
LLookupStart\Function:
// 条件判断,真机是CACHE_MASK_STORAGE_HIGH_16,p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
...
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
ldr p11, [x16, #CACHE] // x16平移CACHE个位置,也就是class平移16字节, p11 = cache_t
#if CONFIG_USE_PREOPT_CACHES // 在arm64架构CONFIG_USE_PREOPT_CACHES = 1
#if __has_feature(ptrauth_calls) // 针对arm64e架构(A12芯片及以上)
tbnz p11, #0, LLookupPreopt\Function
and p10, p11, #0x0000ffffffffffff // p10 = buckets
#else
and p10, p11, #0x0000fffffffffffe // p10 = p11 & 0x0000fffffffffffe 也就是p10 = buckets
tbnz p11, #0, LLookupPreopt\Function // 判断p11是否为空,不为空走LLookupPreopt
#endif
eor p12, p1, p1, LSR #7
and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
and p10, p11, #0x0000ffffffffffff // p10 = p11 & 0x0000ffffffffffff, 也就是 p10 = buckets
and p12, p1, p11, LSR #48 // p11右移48位得到mask,所以 x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
...
#else
#error Unsupported cache mask storage for ARM64.
#endif
#CACHE
的定义
#define CACHE (2 * __SIZEOF_POINTER__) // 2倍的指针大小,也就是 2 * 8 = 16
上面的流程解析
p16 = class
p16
平移16字节,p11 = cache_t
p11 & 0x0000fffffffffffe
得到buckets
,也就是p10 = buckets
x12 = _cmd & mask
,经过hash算法得到buckets
的index
,x12 = index
继续往下看
// #define PTRSHIFT 3
add p13, p10, p12, LSL #(1+PTRSHIFT) // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT)),
(_cmd & mask) << 4就是把地址转换成int,buckets+就是做平移,所以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
#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
- 通过
buckets
平移找到对应下标的bucket
{imp, sel} = *bucket--
,p17 = imp
,p9 = sel
,bucket
继续向前平移- 判断当前的
sel
和要查找的sel
是否相等,如果相等,跳到2
,CacheHit
,找到了,如果不相等,跳到3
- 判断
sel
是否为空,如果为空,跳转MissLabelDynamic
,不为空判断bucket >= buckets
,如果满足跳转到1
,继续循环查找 根据第2
步缓存查找到后,CacheHit
做了什么?再搜索一下CacheHit
的定义:
.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
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
的定义:
.macro TailCallCachedImp
// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
eor $0, $0, $3 // $0就是X17,也就是imp; $3就是x16,也就是class
br $0 // 返回编码后的imp
.endmacro
$0 = imp ^ cls
,imp
和cls
经过异或得到的编码后的imp
- 返回编码后的
imp
总结
- 通过
LLDB
以汇编的方式调试,得到cache
是什么时候插入的 - 通过
Clang
编译成底层源码分析,了解了方法的调用底层是发送消息,通过objc_msgSend
函数发送 - 由
objc_msgSend
找到了消息发送是以汇编语言编写的 - 根据汇编的流程一步步解析
cache
的查找方式