前言
在上一篇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: 通过基地址 + 偏移 获得一个字符串(全局变量)ldpx0,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 = classp16平移16字节,p11 = cache_tp11 & 0x0000fffffffffffe得到buckets,也就是p10 = bucketsx12 = _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的查找方式
























![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)