Runtime
一、什么是 runtime ?
- Objective-C 语言将尽可能多的决策从 编译时和链接时 推迟到运行时。只要有可能,它就 动态 地做事情,这意味着该语言不仅需要一个编译器,还需要一个 运行时系统 来执行编译后的代码。运行时系统作为 Objective-C 语言的一种操作系统,它使语言起作用。
- 因为 Objc 是一门动态语言,所以它总是想办法把一些决定工作从编译连接推迟到 运行时 。也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的代码。这就是 Objective-C Runtime 系统存在的意义,它是整个 Objc 运行框架的一块基石。
- runtime 简称运行时,OC 就是运行时机制,其中最主要的是 消息机制 。对于 C 语言,函数的调用在编译的时候会决定调用哪个函数。对于 OC 的函数,属于 动态调用过程 ,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
- runtime 基本是用 C 和汇编 写的,可见苹果为了动态系统的高效而作出的努力。
二、runtime 版本
- runtime 有两个版本:⼀个 Legacy 版本(早期版本) ,⼀个 Modern 版本(现⾏版本) ;
- 早期版本对应的编程接⼝: Objective-C 1.0;
现⾏版本对应的编程接⼝: Objective-C 2.0。 - 头文件:
#import <objc/message.h>
#import <objc/runtime.h>
三、 runtime 运行方式
- 直接在 OC 层进行交互:比如 @selector;
- NSObjct 的:NSSelectorFromString 方法;
- runtime API: sel_registerName。
四、runtime 运行编译:方法本质
- 定义以下代码:
@interface YDWPerson : NSObject
- (void)goShopping;
- (void)goPay;
@end
@implementation YDWPerson
- (void)goShopping {
NSLog(@"Buy Something");
}
@end
YDWPerson *person = [YDWPerson alloc];
[person goShopping];
[person goPay];
- 编译以上代码,可以完全通过,但是 run 却不能正常执行。为什么会出现这个问题?我们不难发现,是因为 goPay 这个方法没有被实现,只是做了声明。
- 修改以上代码如下:
YDWPerson *person = [YDWPerson alloc];
[person goShopping];
objc_msgSend(person, sel_registerName("goShopping"));
- 然后运行,打印如下:
2020-09-19 00:28:02.688852+0800 YDWObjc[82371:3807707] Buy Something
2020-09-19 00:28:02.689424+0800 YDWObjc[82371:3807707] Buy Something
-
我们写的代码在程序运行过程中都会被转化成 runtime 的 C 代码执行,例如上面的 [person goShopping] ;会被转化成 objc_msgSend(person, sel_registerName(“goShopping”)); ;,它们两者其实是等价的。
-
运用 clang 也可以验证这个结论:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ {
__AtAutoreleasePool __autoreleasepool;
YDWPerson *person = ((YDWPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("YDWPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("goShopping"));
}
return 0;
}
- 由此可见,真正发送消息的地方是 objc_msgSend ,这个方法有基本的两个参数,第一个参数是 消息的接收者为 id 类型 ,第二个参数是 方法编号为 SEL 类型 。
- objc_msgSendSuper 的方法实现如下:
有两个参数 (结构体,sel) ,其结构体类型是 objc_super 定义的结构体对象,且需要指定 receiver 和 super_class 两个属性;
OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
#endif
- objc_super 如下:
#define OBJC_SUPER
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
#endif
- 当一个方法被调用的时候,编译器会根据方法调用的类型生成一个 底层函数 。 调用父类方法生成 objc_msgSendSuper() 函数,非父类方法生成 objc_msgSend() 函数。如果返回值是结构类型,则使用 objc_msgSendSuper_stret 或者 objc_msgSend_stret 。
objc_msgSend
一、objc_msgSend 消息发送
继续使用上面的代码示例,打印出 goShopping 的实现:
- 向 person 发送 goShopping 方法:
YDWPerson *person = [YDWPerson alloc];
objc_msgSend(person, sel_registerName("goShopping"));
- 向 YDWPerson 发送 goShopping 方法;
objc_msgSend((id)objc_getClass("YDWPerson"), sel_registerName("goShopping"));
- 向父类 YDWPerson 发送 goShopping 消息;
YDWPerson *person = [YDWPerson alloc];
struct objc_super ydwSuper;
ydwSuper.receiver = person;
ydwSuper.super_class = YDWPerson.class;
objc_msgSendSuper(&ydwSuper, sel_registerName("goShopping"));
- 上面都会打印出:
2020-09-19 00:57:36.494819+0800 YDWObjc[82683:3833093] Buy Something
2020-09-19 00:57:36.495416+0800 YDWObjc[82683:3833093] Buy Something
2020-09-19 00:57:36.495472+0800 YDWObjc[82683:3833093] Buy Something
2020-09-19 00:57:36.495485+0800 YDWObjc[82683:3833093] Buy Something
- objc_msgSend() :
- 它有两个默认参数 id 类型的 self 和 SEL 类型的 _cmd ,其中 self 指向 消息接收者 , _cmd 是 方法选择器 。如果需要传入更多的参数,可以拼接在这两个参数的后面。
- C 语言的函数指针直接保存了 方法的地址 ,不同于 C 语言的是,SEL 保存的是 消息主体 ,方法 以 SEL 作为 索引 ,通过 SEL 找到 IMP (函数指针)以完成 消息的发送 。
objc_msgSend(id _Nullable self, SEL _Nonnull _cmd, ...)
二、objc_msgSend 的实现
- objc_msgSend() 消息发送的过程就是 通过 SEL 查找 IMP 的过程 。
- objc_msgSend() 是用 汇编语言 实现的,使用汇编实现的优势是:
- 消息发送的过程需要足够的快速,高级语言在执行的时候都是需要翻译成汇编语言,经过编译成被机器识别的 二进制文件 ,使用汇编可以省去这一翻译过程,可以更快速被机器识别;
- 对于消息的发送,存在很多未知的参数,这有很多不确定性,使用 汇编的寄存器 要比 C 或者 C++ 表现好的多。
三、objc_msgSend 汇编源码分析
- objc_msgSend 是消息发送的源码入口,其使用汇编实现的, _objc_msgSend 主要是获取接收者的 isa 信息,源码实现如下:
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
cmp p0, #0 // nil check and tagged pointer check
#if SUPPORT_TAGGED_POINTERS
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
b.eq LReturnZero
#endif
ldr p13, [x0] // p13 = isa
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
CacheLookup NORMAL, _objc_msgSend
#if SUPPORT_TAGGED_POINTERS
LNilOrTagged:
b.eq LReturnZero // nil check
// tagged
adrp x10, _objc_debug_taggedpointer_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
ubfx x11, x0, #60, #4
ldr x16, [x10, x11, LSL #3]
adrp x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGE
add x10, x10, _OBJC_CLASS_$___NSUnrecognizedTaggedPointer@PAGEOFF
cmp x10, x16
b.ne LGetIsaDone
// ext tagged
adrp x10, _objc_debug_taggedpointer_ext_classes@PAGE
add x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
ubfx x11, x0, #52, #8
ldr x16, [x10, x11, LSL #3]
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
ENTRY _objc_msgLookup
UNWIND _objc_msgLookup, NoFrame
cmp p0, #0 // nil check and tagged pointer check
- 源码分析如下:
- cmp p0, #0 :p0 和空对比,即判断接收者是否存在,其中 p0 是 objc_msgSend 的第一个参数,即消息接收者 receiver;
- b.eq LReturnZero : p0 等于 0 时,直接返回空;
- ldr p13, [x0] :p0 即 receiver 流程,根据对象拿出 isa ,即从 x0寄存器 指向的地址取出 isa,存入 p13 寄存器 ;
- GetClassFromIsa_p16 p13 :在64位架构下通过 p16 = isa(p13) & ISA_MASK,拿出 shiftcls 信息,得到 class 信息;
- CacheLookup NORMAL, _objc_msgSend :如果有 isa,执行 CacheLookup(即缓存)查找流程 ,也就是 sel-imp 快速查找流程 ;
- objc_msgSend 消息入口总结如下:
- ① 判断 objc_msgSend 方法的第一个参数 receiver 是否为空:
如果 SUPPORT_TAGGED_POINTERS ,跳转至 LNilOrTagged ;
如果 TAGGED_POINTERS 为空,则直接返回空,即 LReturnZero ;
如果 TAGGED_POINTERS 不为空,则处理 TAGGED_POINTERS 的 isa,执行 CacheLookup NORMAL ; - ② 如果 receiver 不为空,并不支持 SUPPORT_TAGGED_POINTERS,那么就从 receiver 中取出 isa 存入 p13 寄存器 ,通过 GetClassFromIsa_p16 (arm64 架构)中的 isa & ISA_MASK 获取 shiftcls 位域 的类信息,即 class,执行 CacheLookup NORMAL ;
- ① 判断 objc_msgSend 方法的第一个参数 receiver 是否为空:
- GetClassFromIsa_p16 的实现如下:
#if SUPPORT_INDEXED_ISA
.align 3
.globl _objc_indexed_classes
_objc_indexed_classes:
.fill ISA_INDEX_COUNT, PTRSIZE, 0
#endif
.macro GetClassFromIsa_p16 /* src */
#if SUPPORT_INDEXED_ISA
// Indexed isa
mov p16, $0 // 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-bit packed isa
and p16, $0, #ISA_MASK
#else
// 32-bit raw isa
mov p16, $0
#endif
.endmacro
四、CacheLookup 缓存查找分析(快速查找)
- ① 通过 cache 首地址平移 16 字节(在 objc_class 中,首地址距离 cache 正好 16 字节,即 isa 占 8 字节,superClass 占 8 字节)获取 cahce,cache 中高 16 位存 mask,低 48 位存 buckets,即 p11 = cache:
.macro CacheLookup
//
// Restart protocol:
//
// As soon as we're past the LLookupStart$1 label we may have loaded
// an invalid cache pointer or mask.
//
// When task_restartable_ranges_synchronize() is called,
// (or when a signal hits us) before we're past LLookupEnd$1,
// then our PC will be reset to LLookupRecover$1 which forcefully
// jumps to the cache-miss codepath which have the following
// requirements:
//
// GETIMP:
// The cache-miss is just returning NULL (setting x0 to 0)
//
// NORMAL and LOOKUP:
// - x0 contains the receiver
// - x1 contains the selector
// - x16 contains the isa
// - other registers are set as per calling conventions
//
LLookupStart$1:
// p1 = SEL, p16 = isa
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
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
- 源码分析:
- ldr p11, [x16, #CACHE] :
p1 = SEL,p16 = isa,#define CACHE (2 * SIZEOF_POINTER),其中 __SIZEOF_POINTER__表示 pointer 的大小 ,即 2*8 = 16;
p11 = mask|buckets ,从 x16(即 isa )中平移 16 个字节,取出 cache 存入 p11 寄存器,而 isa 距离cache 正好 16 字节:isa(8字节、superClass(8字节);
cache(mask 高 16 位 + buckets 低 48 位); - and p10,p11,#0x0000ffffffffffff : mask 高 16 位抹零,得到 buckets 存入 p10 寄存器, 即去掉 mask,留下 buckets;
- and p12, p1, p11, LSR #48 :p11(cache) 右移 48 位,得到 mask(即 p11 存储 mask),mask & p1(msgSend 的第二个参数 cmd-sel) ,得到 sel-imp 的下标 index(即搜索下标) 存入p12(cache insert 写入时的哈希下标计算是通过 sel & mask,读取时也需要通过这种方式);
- ldr p11, [x16, #CACHE] :
- 类的结构排布为 isa、superclass、cache_t、class_data_bits ,这里要进行的是查找缓存的流程,缓存的信息是存储在 cache_t 中的。x16 是上一步中获取到的类信息,x16 偏移 16 字节就是取到 cache_t 结构,存入 p11 中。
#define CACHE (2 * __SIZEOF_POINTER__)
ldr p11, [x16, #CACHE]
-
p11 为 _maskAndBuckets ,它的低 48 位存储 buckets , 高16位存储 mask,#0x0000ffffffffffff 转为二进制如下:
-
② 从 cache 中分别取出 buckets 和 mask,并由 mask 根据哈希算法计算出哈希下标:
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
and p10, p11, #0x0000ffffffffffff // p10 = buckets
and p12, p1, p11, LSR #48 // x12 = _cmd & mask
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
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
- 源码分析:
- 通过 cache 和 0x0000ff ffffffffff 的 & 运算,将高 16 位 mask 抹零,得到 buckets 指针地址,即 p10 = buckets;
- 将 cache 右移 48 位,得到 mask,即 p11 = mask;
- 将 objc_msgSend 的参数p1(即第二个参数_cmd)& mask,通过哈希算法得到需要查找存储 sel-imp 的 bucket 下标 index,即 p12 = index = _cmd & mask。在存储 sel-imp 时,也是通过同样哈希算法计算哈希下标进行存储,所以读取也需要通过同样的方式读取;
static inline mask_t cache_hash(SEL sel, mask_t mask)
{
return (mask_t)(uintptr_t)sel & mask;
}
- ③ 根据所得的 哈希下标 index 和 buckets 首地址,取出 哈希下标 对应的 bucket;
- PTRSHIFT 等于 3,左移 4 位(即 24 = 16 字节),目的是计算出 bucket 实际占用的大小,结构体 bucket_t 中 sel 占 8 字节,imp 占 8 字节;
- 根据计算的 哈希下标 index 乘以 单个 bucket 占用的内存大小 ,得到 buckets 首地址在实际内存中的偏移量;
- 通过首地址 + 实际偏移量,获取哈希下标 index 对应的 bucket;
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
3: // wrap: p12 = first bucket, w11 = mask
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
add p12, p12, p11, LSR #(48 - (1+PTRSHIFT))
// p12 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
add p12, p12, p11, LSL #(1+PTRSHIFT)
// p12 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
ldp p17, p9, [x12] // {imp, sel} = *bucket
1: cmp p9, p1 // if (bucket->sel != _cmd)
b.ne 2f // scan more
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
CheckMiss $0 // miss if bucket->sel == 0
cmp p12, p10 // wrap if bucket == buckets
b.eq 3f
ldp p17, p9, [x12, #-BUCKET_SIZE]! // {imp, sel} = *--bucket
b 1b // loop
LLookupEnd$1:
LLookupRecover$1:
3: // double wrap
JumpMiss $0
.endmacro
- 源码分析:
- add p12, p10, p12, LSL #(1+PTRSHIFT) :p12 是下标 p10 是 buckets 数组首地址,下标 * 1<<4(即16) 得到实际内存的偏移量,通过 buckets 的首地址偏移,获取 bucket 存入 p12 寄存器;LSL #(1+PTRSHIFT) 就是得到一个 bucket 占用的内存大小 ,相当于 mask = occupied -1,即 _cmd & mask 取余;
- ldp p17, p9, [x12] :从x12(即p12)中取出 bucket 分别将 imp 和 sel 存入 p17(存储imp) 和 p9(存储sel);
- 1: cmp p9, p1 :比较 sel 与 p1(传入的参数cmd);
- b.ne 2f :如果不相等,
- CacheHit $0 :如果相等即命中,直接返回 imp;
- CheckMiss $0 :如果一直找不到 buckets ,则 CheckMiss;
- cmp p12, p10 :判断 p12(下标对应的 bucket ) 是否等于 p10(buckets数组第一个元素);
- b.eq 3f :如果等于,获取哈希下标对应的bucket;
- ldp p17, p9, [x12, #-BUCKET_SIZE]! :从x12(即p12 buckets 首地址)- 实际需要平移的内存大小BUCKET_SIZE,得到得到第二个bucket元素,imp-sel分别存入p17-p9,即向前查找;
- b 1b :继续对比 sel 与 cmd;
- ④ 根据获取的 bucket,取出其中的 sel 存入 p17,即 p17 = sel ,取出 imp 存入 p9,即 p9 = imp ;
- ⑤ 比较获取的 bucket 中 sel 与 objc_msgSend 的第二个参数的 _cmd(即p1) 是否相等;
- 如果相等,则直接跳转至 CacheHit ,即 缓存命中 ,返回 imp;
- 如果不相等,则:如果一直都找不到,直接跳转至 CheckMiss ,因为 $0 是 normal,会跳转至 __objc_msgSend_uncached ,即进入慢速查找流程;如果根据 index 获取的 bucket 等于 buckets 的第一个元素,则将当前 bucket 设置为 buckets 的最后一个元素(通过 buckets 首地址 + mask 右移 44 位(等同于左移 4 位)直接定位到 buckets 的最后一个元素),然后继续向前查找;
- CacheHit 源码如下:
.macro CacheHit
.if $0 == NORMAL
TailCallCachedImp x17, x12, x1, x16 // authenticate and call imp
.elseif $0 == GETIMP
mov p0, p17
cbz p0, 9f // don't ptrauth a nil imp
AuthAndResignAsIMP x0, x12, 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, x12, x1, x16 // authenticate imp and re-sign as IMP
ret // return imp via x17
.else
.abort oops
.endif
.endmacro
.macro CheckMiss
// miss if bucket->sel == 0
.if $0 == GETIMP
cbz p9, LGetImpMiss
.elseif $0 == NORMAL
cbz p9, __objc_msgSend_uncached
.elseif $0 == LOOKUP
cbz p9, __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
.macro JumpMiss
.if $0 == GETIMP
b LGetImpMiss
.elseif $0 == NORMAL
b __objc_msgSend_uncached
.elseif $0 == LOOKUP
b __objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro
- ⑥ objc_msgSend 快速查找 赋值变化 流程
- ⑦ 重复⑤的操作,与⑤中唯一区别是,如果当前的 bucket 等于 buckets 的第一个元素,则直接跳转至 JumpMiss ,此时的 $0 是 normal ,也是直接跳转至 __objc_msgSend_uncached ,即进入慢速查找流程;
- ⑧ objc_msgSend 快速查找整体流程
五、__objc_msgSend_uncached 慢速查找分析
- __objc_msgSend_uncached 源码实现如下:
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band p16 is the class to search
MethodTableLookup
TailCallFunctionPointer x17
END_ENTRY __objc_msgSend_uncached
- __objc_msgSend_uncached 中最核心的逻辑就是 MethodTableLookup(即查询方法列表) ,因为缓存并没有命中,这里是开始去 方法列表 (methodList) 的查找流程;
.macro MethodTableLookup
// push frame
SignLR
stp fp, lr, [sp, #-16]!
mov fp, sp
// save parameter registers: x0..x8, q0..q7
sub sp, sp, #(10*8 + 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8, [sp, #(8*16+8*8)]
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward
// IMP in x0
mov x17, x0
// restore registers and return
ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16+0*8)]
ldp x2, x3, [sp, #(8*16+2*8)]
ldp x4, x5, [sp, #(8*16+4*8)]
ldp x6, x7, [sp, #(8*16+6*8)]
ldr x8, [sp, #(8*16+8*8)]
mov sp, fp
ldp fp, lr, [sp], #16
AuthenticateLR
.endmacro
- 可以看出,执行查找流程的是 _lookUpImpOrForward 。 至此,objc_msgSend() 的 快速查找流程 就结束了,接下来进入的 慢速查找流程 ,也就是通过 isa 与 superclass 的指向,一层一层寻找下去,直到整个 objc_msgSend() 发送完毕。