iOS 底层第8天的学习。今天的内容是分析objc_msgSend有点难,需要一步步去分析,最后得出一个流程。
objc_msgSend 分析
- 在底层源码中搜索
objc_msgSend找到arm64架构下的objc-msg-arm64.s文件,点击进入开始分析汇编。 - 开始分析前说明一下:所有代码分析都是针对
arm64架构的。

- 接下来我们开始分析
cmp p0, #0
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
// 补充 LNilOrTagged
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
GetTaggedClass
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
p0 = receiver,cmp判断消息接受者是否存在,#0= 当消息接受者不存在 进入直接返回
LReturnZero。 这里SUPPORT_TAGGED_POINTERS在arm64就返回1,也就是会进入LNilOrTagged,而LNilOrTagged也会返回LReturnZero->LGetIsaDone说明已经找到了isa.
- 当消息接受者存在的时候继续走下面的代码
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
ldr p13, [x0]将存储器地址为x0的字数据读入寄存器p13。p13-> 对象的首地址->isa,x0->isaGetClassFromIsa_p16 p13, 1, x0把参数传入GetClassFromIsa_p16
搜索GetClassFromIsa_p16找到macro定义
.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
GetClassFromIsa_p16 参数表
| 参数名 | 参数传入的值 |
|---|---|
src |
p13(isa) |
needs_auth |
1 |
auth_address |
x0(isa) |
- 我们看下
SUPPORT_INDEXED_ISA这个判断条件
#if __ARM_ARCH_7K__ >= 2 || (__arm64__ && !__LP64__)
# define SUPPORT_INDEXED_ISA 1
#else
# define SUPPORT_INDEXED_ISA 0
#endif
1:
__ARM_ARCH_7K__根据名称可以得出是定义在目标为 ARM 7k 架构 CPU ,这里应该主要是针对watchOS的,不满足条件。2:
(__arm64__ && !__LP64__)在真机环境__arm64__=1,__LP64__=1,1 && 0=0,因此SUPPORT_INDEXED_ISA = 0继续往下分析
needs_auth = 0肯定不成立,我们看GetClassFromIsa_p16参数表可知needs_auth =1.if判断不成立 =>ExtractISA p16, \src, \auth_address,把p16(空地址),src,auth_address传入到ExtractISA。接着全局搜索ExtractISA
// A12 不是普遍版本
#if __has_feature(ptrauth_calls)
// JOP
// ... 省略部分代码
.macro ExtractISA
and $0, $1, #ISA_MASK
#if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_STRIP
xpacd $0
#elif ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
mov x10, $2
movk x10, #ISA_SIGNING_DISCRIMINATOR, LSL #48
autda $0, x10
#endif
.endmacro
// JOP
#else
.macro ExtractISA
and $0, $1, #ISA_MASK
.endmacro
// not JOP
#endif
-
#if __has_feature(ptrauth_calls)只有A12架构会进入,不是普遍版本,直接看#else下面的代码
and $0, $1, #ISA_MASK 参数表
| 参数名 | 参数传入的值 |
|---|---|
$0 |
p16 |
$1 |
src= p13(isa)
|
-
and $0, $1, #ISA_MASK=>isa & ISA_MASK = p16 - 就这样我们就得到了
GetClassFromIsa_p16 p13, 1, x0=>p16 = class - p16 = class
- 继续往下进行分析
LGetIsaDone: // 函数名
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
- 搜索
CacheLookup如下
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
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, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
CacheLookup 参数表
| 参数名 | 参数传入的值 |
|---|---|
Mode |
NORMAL |
Function |
_objc_msgSend |
MissLabelDynamic |
__objc_msgSend_uncached |
MissLabelConstant |
nil = MissLabelConstant |
-
mov x15, x16, 把p16 isa移到x15,x16 = p16, -
LLookupStart\Function:查找Function = _objc_msgSend - 在
arm64架构下直接看#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16判断下的代码 -
ldr p11, [x16, #CACHE],CACHE多少不知道,搜索找到如下代码
#define CACHE (2 * __SIZEOF_POINTER__) // __SIZEOF_POINTER__ = 8, CACHE = 16
-
ldr p11, [x16, #CACHE]=>x16 class平移16找到cache_t, 把cache_t存到p11里。 p11 = cache_t-
#if 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
-
arm64=>CONFIG_USE_PREOPT_CACHES=1,继续看代码 -
#if __has_feature(ptrauth_calls)只有A12架构会进入,直接进入#else -
and p10, p11, #0x0000fffffffffffe=>p11 & #0x0000fffffffffffe=>buckets, p10 = buckets-
tbnz p11, #0, LLookupPreopt\Function——> 当前的p11与#0作比较,不等于0就跳转LLookupPreopt. - 继续分析,已经知道
p10 = buckets,我们的目的是为了找到buckets里的bucket,再根据x1 = sel与bucket里的sel进行比对, 找到相应的imp.
eor p12, p1, p1, LSR #7
and p12, p1, p11, LSR #48`
-
eor p12, p1, p1, LSR #7=>_cmd ^ _cmd >> 7=value ^= value >> 7
- 为什么要在读的时候
_cmd要右移7呢?
- 因为在
insert时计算begin的时, 在cache_hash函数value ^= value >> 7,所以在读的时候也要进行相同的操作。 -
p11, LSR #48=>p11(cache_t)右移48得到了mask.
- 为什么 要右移
48位就得到了mask呢?
- 因为 总共
64位,前16位存了 mask ,后 48 位存了buckets,当右移动48位 就把buckets移除了,所以 右移48得到了mask. - ->
and p12, p1, mask->p1(_cmd ^ _cmd >> 7) & mask = p12. - 那这个
p12存的是什么呢?回到cache_t中的源码进行查看
// insert 函数里 计算 begin
mask_t m = capacity - 1;
mask_t begin = cache_hash(sel, m);
mask_t i = begin;
// cache_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);
}
- 分析源码得知在
insert时候begin = cache_hash(sel, m);根据mask后得到的begin, 和p1 & mask=p12其实是计算index, 会根据index去找bucket. - 当我们得到
p12存了index后继续往下分析
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
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
// p13 = buckets + (mask << 1+PTRSHIFT)
// see comment about maskZeroBits
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = first probed bucket
-
add p13, p10, p12, LSL #(1+PTRSHIFT),搜索已知PTRSHIFT = 3,p10 = buckets - =>
p12, LSL #(1+PTRSHIFT)=index << 4=> 可以理解为地址的平移, 相当于buckets[i]
- 为什么
index << 4要左移4位呢?
- 举个 🌰🌰🌰
index = 2
index = 2
index << 4 => 00 10 << 4 = 10 0000 = 32
32 = 2 * 16 => 向左平移了 2 个 bucket(sel,imp) 的大小
平移的目的就为了找到 bucket
- =>
add p13, p10,(index << 4)=>p13 = buckets[i] = 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
- 这里
1:,2:,3:就相等于一个do white循环 -
1: ldp p17, p9, [x13], #-BUCKET_SIZE,[x13] = bucket进行#-BUCKET_SIZE = bucket--,ldp就是将[x13]偏移BUCKET_SIZE = 16个字节的值取出来,放入p17和p9。这里的p9 = sel,p17 = imp. -
cmp p9, p1=> 比较sel与_cmd- 如果不相同调用
b.ne 3f就会调用3:cbz p9, \MissLabelDynamic-
3:cbz p9, \MissLabelDynamic=> 寻找p9,如果p9 = 0调用MissLabelDynamic = __objc_msgSend_uncached
-
- 如果相同调用
CacheHit \Mode,搜索下CacheHit找到macro
- 如果不相同调用
.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
- 参数
Mode = NORMAL,$0 == NORMAL = true -
x17 = imp,x10 = buckets,x1 = sel,x16 = isa = class TailCallCachedImp x17, x10, x1, x16- 搜索
TailCallCachedImp继续分析
.macro TailCallCachedImp
// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
eor $0, $0, $3
br $0
.endmacro
eor $0, $0, $3=>$0 = cached imp,$3 = isa,imp^isa=>imp code再存入$0br $0=>call imp继续
cmp p13, p10=>p13 = bucket与p10 = buckets 首地址进行比较,如果bucket >= buckets调用b.hs 1b继续进行内存平移。如果
bucket已经走到了0好位置,还不相等 怎么办呢?继续往下分析
add p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
// p13 = buckets + (mask << 1+PTRSHIFT)
// see comment about maskZeroBits
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 + (mask << 1+PTRSHIFT)=>mask向左移动了4位置 =>7 * 16=>p13定位到最后一个的位置。 -
add p12, p10, p12, LSL #(1+PTRSHIFT)=>p12 = first probed bucket ccmp p13, p12, #0, ne // bucket > first_probed)
- 为什么
bucket > first_probed?
因为之前已经比较过一次,所以这里就必须大于
first_probed否则还要走一次进行比较以上分析就是
sel找imp的过程
objc_msgSend 流程
-
objc_msgSend主要是通过SEL _cmd找到bucket里sel,通过compare最后锁定到imp,这一步步的查找过程到底是如何的,我们在这里画个流程图再具体梳理下

