1.OC方法调用的本质
将一段简单的方法调用代码,放在main方法中,然后使用clang命令编译。
ZPerson *person = [ZPerson alloc];
[person sayHello];
复制代码
然后使用clang -rewrite-objc main.m命令编译。得到如下编译后的源码:
ZPerson *person = ((ZPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("ZPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
复制代码
去掉强制类型转换后:
objc_msgSend(objc_getClass("ZPerson"), sel_registerName("alloc"));
objc_msgSend(person, sel_registerName("sayHello"));
复制代码
通过上面这段代码,我们发现不管是alloc
方法调用还是sayHello
方法的调用,都是通过objc_msgSend
函数来实现的。
因此我们可以说OC方法的本质就是通过objc_msgSend
来发送消息。
objc_msgSend
包含有方法的调用的两个隐藏参数:self(消息接受者)
和sel(方法编号)
。
sel_registerName
等同于oc中的@selector()
,可以根据传进的方法名得到一个sel
。
2.objc_msgSend
2.1 objc_msgSend的方法实现
在源码总查找objc_msgSend
方法,只能找到方法定义的声明,不能点进去看到方法的实现。
OBJC_EXPORT void
objc_msgSend(void /* id self, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
复制代码
这是因为objc_msgSend
是由汇编语言实现的。原因有二:
- 从性能方面考虑,方法调用需要被快速的处理和响应,而汇编更容易被机器识别。
- 由于未知参数的原因(个数未知、类型未知,比如NSLog()),c和c++作为静态语言,并不能满足这一特性。
根据一条约定俗成的原则,我们在objc_msgSend
前加上下划线,我们继续搜索_objc_msgSend
,以期找到objc_msgSend
的汇编源码。
根据不同的平台,obcj提供了不同版本的方法实现,我们选择arm64版本进行分析。ENTRY
为方法的入口标志,我们将从开始入手分析。
2.2 _objc_msgSend
在objc-msg-arm64.s
中找到ENTRY _objc_msgSend
,这是_objc_msgSend
函数的入口。本人汇编水平有限,根据网上查找的资料,源码做了如下注释。
//_objc_msgSend 方法入口
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
// p0 和 0 比较,即判断接收者是否存在,
// 其中 p0 是 objc_msgSend 的第一个参数,消息接收者 receiver
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS //
// 支持 tagged pointer 的流程, 并且比较的结果 le 小于或等于
// 跳转到 LNilOrTagged 标签处执行 Taggend Pointer 对象的函数查找及执行
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
// p0 等于 0 的话,则跳转到 LReturnZero 标签处
// LReturnZero 置 0 返回 nil 并直接结束 _objc_msgSend 函数
b.eq LReturnZero
#endif
// 不过方法接收者不为nil 就会继续向下执行
// p0 即 receiver 肯定存在的流程,实际规定是 p0 - p7 是接收函数参数的寄存器
// 从 x0 寄存器指向的地址取出 isa,存入 p13 寄存器
ldr p13, [x0] // p13 = isa
// 从 isa 中获取类指针并存放在通用寄存器 p16 中
GetClassFromIsa_p16 p13, 1, x0 // p16 = class
// 本地标签(表示获得 isa 完成)
LGetIsaDone:
// calls imp or objc_msgSend_uncached
// 如果有 isa,走到 CacheLookup 即缓存查找流程,也就是所谓的 sel-imp 快速查找流程,
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
// nil 检测,如果是 nil 的话也跳转到 LReturnZero 标签处
b.eq LReturnZero // nil check
//从tagged pointer指针中查找class, 并存放到x16中
GetTaggedClass
b LGetIsaDone
// SUPPORT_TAGGED_POINTERS
#endif
LReturnZero:
// x0 is already zero
// 置 0
mov x1, #0
movi d0, #0
movi d1, #0
movi d2, #0
movi d3, #0
// return 结束执行
ret
// LExit 结束 _objc_msgSend 函数执行
END_ENTRY _objc_msgSend
复制代码
前面我们已经分析对象和类的结构,此时对照前面的知识,再来看_objc_msgSend
的方法流程,应该不会陌生了。
首先是检查方法接收者是否为nil。
然后是从对象中获取isa
, 然后根据isa
获取class
。我们知道,对象的方法是存在class
里的。
拿到class
后,就可以去根据方法签名(sel)去查找方法的实现(imp)了。
在class
中查找imp,有两种途径:
- 一是利用
cache_t
,查看缓存中有没有,也就是常说的快速查找 - 二是利用
class_data_bits_t
,一步步深入查找,可以参考前面的类的结构之数据存储。效率不如第一种方式高,也叫慢速查找
objc_msgSend
的大致流程就是这些了,可以结合下图理一下思路。
具体方法的实现的查找流程,请看下面对CacheLookup
的分析。
3.CacheLookup
CacheLookup NORMAL|GETIMP|LOOKUP <function> MissLabelDynamic MissLabelConstant
复制代码
NORMAL|GETIMP|LOOKUP
分别代表三种不同模式:
NORMAL
,正常模式,找到对应的IMP
, 并执行GETIMP
,找到IMP
, 并返回LOOKUP
,仅查找IMP
<function>
代表了在哪个函数中使用CacheLookup
代码块。比如_objc_msgSend
函数中,使用时,传递过来的就是_objc_msgSend
。
CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached
复制代码
MissLabelDynamic
、 MissLabelConstant
:当没能找到缓存时,执行的代码块,在外部执行CacheLookup
时传入。MissLabelConstant
仅在GETIMP
模式下使用。
当CacheLookup
准备执行时,p1 = sel, p13 = isa,p16 = class
当执行结束时,如果从缓存中找到对应的IMP,x16 = class, x17 = IMP。如果没有找到,x15 = class
3.1 CacheHit
CacheHit
是缓存命中时,指定的代码宏定义。
缓存命中:x17 IMP的地址, x10 buckets 的地址, x1 中存的是SEL, x16 中保存类指针
.macro CacheHit
.if $0 == NORMAL // NORMAL 表示通常情况下在缓存中找到了函数执行并返回
// TailCallCachedImp 定义在 arm64-asm.h 中
// 验证并执行 IMP
TailCallCachedImp x17, x10, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP // GETIMP 仅在缓存中查找 IMP
// p17 中是 cached IMP,放进 p0 中
mov p0, p17
// cbz 比较(Compare),如果结果为零(Zero)就转移(只能跳到后面的指令)
// 如果 p0 是 0,则跳转到 标签 9 处,标签 9 处直接执行 ret
cbz p0, 9f // don't ptrauth a nil imp
AuthAndResignAsIMP x0, x10, x1, x16 // authenticate imp and re-sign as IMP
// return IMP
9: ret // return IMP
.elseif $0 == LOOKUP // 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.
// 不去检测imp是否为nil, 也不关心跳转是否会失败
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 停止汇编
// Linux内核在发生 kernel panic 时会打印出 Oops 信息,
// 把目前的寄存器状态、堆栈内容、以及完整的 Call trace 都 show 给我们看,
// 这样就可以帮助我们定位错误。
.abort oops
.endif
.endmacro// 结束 CacheHit 汇编宏定义
复制代码
3.2 TailCallCachedImp
当缓存命中时,如果缓存的查找模式是NORMAL
,就会执行TailCallCachedImp
。
验证并执行 IMP。
.macro TailCallCachedImp
// eor 异或指令(exclusive or)
// eor 指令的格式为:eor{条件}{S} Rd,Rn,operand
// eor 指令将 Rn 的值与操作数 operand 按位逻辑”异或”,
// 相同为 0,不同为 1,结果存放到目的寄存器 Rd 中。
// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
// 把 SEL 和 imp 的地址按位进行异或操作,
// 并把结果放在 $1 中 (混合 SEL 到 ptrauth modifier 中)
eor $1, $1, $2 // mix SEL into ptrauth modifier
// 把 isa 和 $1 按位进行异或的操作放在 $1 中 (混合 isa 到 ptrauth modifier 中)
eor $1, $1, $3 // mix isa into ptrauth modifier
// 验证 ptrauth modifier,验证通过就跳转到 $0分支中执行
brab $0, $1
.endmacro
复制代码
3.3 __objc_msgSend_uncached
__objc_msgSend_uncached
是在_objc_msgSend
中查找方法缓存是,传入的方法。当没有找到缓存中没有找到对应的imp
时,就会执行__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
复制代码
3.4 CacheLookup
下面开始进入本节的正题——CacheLookup
。
.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.
// 一旦超过 LLookupStart$1 标签,我们可能已经加载了无效的 缓存指针 或 掩码。
//
// 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:
// 当我们在超过 LLookupEnd$1 之前(或当 信号 命中我们)调用task_restartable_ranges_synchronize(),我们的 PC 将重置为 LLookupRecover$1,这将强制跳转到缓存未命中的代码路径,其中包含以下内容。
//
// GETIMP:
// The cache-miss is just returning NULL (setting x0 to 0)
// 缓存未命中只是返回 NULL
//
// NORMAL and LOOKUP:
// - x0 contains the receiver // x0 存放函数接收者 (就是我们日常的 self)
// - x1 contains the selector // x1 存放 SEL (就是我们日常的 @selector(xxxx))
// - x16 contains the isa // x16 是 class 的 isa (也就是 self 的 isa,根据它来找到对象所属的类)
// - other registers are set as per calling conventions // 其它寄存器根据调用约定来设置
//
//将原始的isa存储到x15
mov x15, x16 // stash the original isa
LLookupStart\Function:
// p1 = SEL, p16 = isa
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
//#define CACHE (2 * __SIZEOF_POINTER__) 就是16
//将存储器地址为(x16+16)的字数据读入寄存器 p10
//类的地址偏移16个字节后,刚好是cache_t的起始地址,地址上的数据, 也就是cache_t的第一个成员变量_bucketsAndMaybeMask
//将cache的内容读取到p10
ldr p10, [x16, #CACHE] // p10 = mask|buckets
//p10存储的数据右移48位 后 存入p11, 此时11为mask
lsr p11, p10, #48 // p11 = mask
//p10 和 bucketsmask 与运算后 再存入p10 此时p10存的是buckets地址
and p10, p10, #0xffffffffffff // p10 = buckets
//w小端模式 x1是sel x11是mask
//计算后 x12存的的当前sel经过hash计算后得到的key
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, 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
// 在 Project Headers/arm64-asm.h 中可以看到 PTRSHIFT 的宏定义
/*
#if __arm64__
#if __LP64__ // 64 位系统架构
#define PTRSHIFT 3 // 1<<PTRSHIFT == PTRSIZE // 0b1000 表示一个指针 8 个字节
// "p" registers are pointer-sized
// true arm64
#else
// arm64_32 // 32 位系统架构
#define PTRSHIFT 2 // 1<<PTRSHIFT == PTRSIZE // 0b100 表示一个指针 4 个字节
// "p" registers are pointer-sized
// arm64_32
#endif
*/
// p12, LSL #(1+PTRSHIFT) 将p12(key) 逻辑左移4位, 左移相等于 *16(bucket大小), 又由于key相当于bucket在bucket中的下标, 所以这一步就是 计算出内存偏移量
// p10 是buekets的首地址, 与偏移量相加 ,计算出bucket 实际的内存地址, 存到p13
// p13 就是buckets中下标为key的元素的 地址
add p13, p10, p12, LSL #(1+PTRSHIFT)
// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// do {
//ldp 出栈指令(`ldr` 的变种指令,可以同时操作两个寄存器)
//将x13偏移BUCKET_SIZE (16) 个字节的内容取出来, 分别存入x17 和 x9
//p17是imp p9是sel
//然后将bucket指针前移一个单位长度
1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket--
//比较sel和cmd 比较从buckets中取出的sel和传入的sel是否相等
cmp p9, p1 // if (sel != _cmd) {
//如果不相等, 就跳转到3执行
b.ne 3f // scan more
// } else {
//如果相等 缓存命中 跳转到CacheHit执行
2: CacheHit \Mode // hit: call or return imp
// }
//如果p9为0 取出的sel为空 未找到缓存 就跳转到 __objc_msgSend_uncached执行
3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss;
//比较bucket地址和buckets地址
//当bucket >= buckets 还成立时 跳回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
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
LLookupEnd\Function:
//最后强制跳转到cache miss
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
复制代码
CacheLookup
的流程大致如下:
- 先将原始的isa存储到x15
- 再经过对
class
的一系列位移操作,得到存放缓存的buckets
- 对当前
sel
进行哈希计算,得到对应下标key - 根据下标,取出
buckets
中对应的缓存bucket
,同时也拿到了bucket
的sel
和imp
- 然后就是一些判断:
- 如果取出的
sel
为空,说明未缓存当前的sel
,跳到__objc_msgSend_uncached
- 如果取出的
sel
和当前的sel
相等,说明缓存命中,跳到CacheHit
- 如果
sel
不为空,且和当前的sel
相等不相等,说明发生了哈希碰撞,可能还是有缓存的,继续哈希探测,取出key
下标前面的bucket
,继续进行判断
- 如果取出的
- 如果全部遍历完
buckets
, 还是没有发生缓存命中,那么遍历一遍buckets
进行查找。可能由于多线程加锁的原因,其他线程向缓存中添加了当前线程正在调用的方法。如果这次找到了,跳到CacheHit
执行 - 如果还是没找到,本段代码也执行到最后了,跳到
__objc_msgSend_uncached
执行。