OC底层原理-objc 818(五)objc_msgSend方法快速查找
编译时与运行时
编译时
编译时顾名思义就是正在编译的时候,就是编译器帮你把源代码翻译成机器能识别的代码。实际上只是翻译成某个中间状态的语言。
那编译时就是简单的做一些翻译工作,比如检查代码规范、语法分析之类的过程。
运行时
所谓运行时就是代码跑起来了,被装在到内存中去了,是一个动态过程,而运行时类型检查就是与前面讲的编译时类型检查不一样,不是简单的扫描代码,而是在内存中做了实际操作进行判断。
OC的运行时就是我们所说的RunTime。
Runtime交互的三种方式
Objective-C Code直接调用
比如直接调用方法[self say]、#selector()等。
Framework&Serivce
比如NSSelectorFromString、isKindOfClass、isMenberOfClass等方法。
RuntimeAPI
比如sel_registerName、class_getInstanceSize等底层方法。
Clang编译OC源代码
环境准备
OC源代码
@interface Person : NSObject - (void) running; - (void) swimming; @end @implementation Person - (void) running { NSLog(@"running"); } - (void) swimming { NSLog(@"swimming"); } @end @interface Student : Person @end @implementation Student @end int main(int argc, const char * argv[]) { @autoreleasepool { Person *person = [Person alloc]; [person running]; objc_msgSend(person, sel_registerName("running")); Student *student = [Student alloc]; [student swimming]; struct objc_super yjSuper; yjSuper.receiver = student; yjSuper.super_class = objc_getClass("Person"); objc_msgSendSuper(&yjSuper, sel_registerName("swimming")); } return 0; }复制代码
Clang转换main.cpp
int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")); ((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("running")); } return 0; }复制代码
方法调用分解
使用objc_msgSend调用running方法
objc_msgSend(person, sel_registerName("running"));
使用objc_msgSendSuper调用父类的swimming方法
objc_msgSendSuper(&yjSuper, sel_registerName("swimming"));
我们看一下objc_msgSendSuper的第一个参数是struct objc_super *结构体指针,源码如下:
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 */ };复制代码
其中我们用到的两个参数分别是receiver
和super_class
,分别是消息接受者,和父类对象,消息接受者就是我们对象本身,父类就是Person类对象,由于我们声明的是一个结构体,参数需要是一个指针所以使用 &
获取结构体地址作为参数传入。
main.cpp分析方法调用过程
通过cpp文件分析我们可以看到alloc方法和running都是通过objc_msgSend方法调用的。
objc_getClass()就是runtime的方法,用于获取Person的类对象。
sel_registerName()也是runtime的方法,用于获取方法,对应OC的@Selector()、NSSelectorFromString()。
我们通过上述发现,无论是实例方法的调用还是类方法的调用都是通过objc_msgSend方法进行的,只是参数一的消息接收者不同,调用类方法时消息接收者是类对象,调用实例方法时消息接收者时实例对象,参数二就是我们要调用的方法。
objc_msgSend会根据参数一在缓存中和方法列表中进行方法查找。
在缓存中查找方法是最快的,所以我们称之为方法快速查找。
Objc_msgSend
介绍
在objc4源码中通过搜索发现objc_msgSend是使用汇编实现的
,汇编的主要特征是:
速度快,汇编更容易被机器识别。
方法参数的动态性,汇编调用函数时传入的参数是不确定的,那么消息发送时,直接调用一个函数就可以发送所有消息。
消息查找机制
快速查找
:cache中查找(缓存查找)。
慢速查找
:methodList中查找(方法列表),和消息转发
Objc_msgSend快速查找分析
objc_msgSend调用
objc_msgSend(person, sel_registerName("running"));
传入两个参数,分别是消息接受者和消息的sel。
objc_msgSend汇编源码
ENTRY _objc_msgSend UNWIND _objc_msgSend, NoFrame // p0是我们传入的第一个参数:消息接受者 // cmp是比较方法,比较p0是否为nil,如果为nil说明没有消息接受者,直接返回 cmp p0, #0 // nil check and tagged pointer check #if SUPPORT_TAGGED_POINTERS // TagPointer类型 b.le LNilOrTagged // (MSB tagged pointer looks negative) #else // 消息接受者为空返回空 b.eq LReturnZero #endif // p13 是获取消息接受者的首地址,也就是isa ldr p13, [x0] // p13 = isa // GetClassFromIsa_p16 通过isa获取类对象并赋值给p16 GetClassFromIsa_p16 p13, 1, x0 // p16 = class LGetIsaDone: // calls imp or objc_msgSend_uncached // 在cache中查找imp CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached复制代码
流程图
CacheLookup源码
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 // 通过isa内存平移16位,获取cache首地址,cache首地址就是maskAndBuckets 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 // maskAndBuckets是一个共用体,共占8位64字节 高16字节存储着mask值低48字节存储着buckets信息 // 所以此处将mask|buckets & 0x0000ffffffffffff 获取低48字节的信息,也就是获取buckets并赋值给p10 and p10, p11, #0x0000ffffffffffff // p10 = buckets // 此处_cmd & mask就是缓存插入式hash值的计算方式,catch_hash的原理就是 _cmd & mask // 所以次数获取的是缓存中的方法hash值,并赋值给p12变量 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 // 根据hash值索引逻辑计算要获取的bucket // 我们知道bucket中存储着sel和imp,所以占16字节 // 那么p12就是_cmd & mask就是索引,也就是当前要找的第几位 // (1+PTRSHIFT) == 4 // (_cmd & mask) << (1+PTRSHIFT) 左移4位相当于,乘以2的4次方 也就是乘以16,整好是每个bucket的大小 // 逻辑运算后p13就是我们当前从缓存中找到的bucket add p13, p10, p12, LSL #(1+PTRSHIFT) // p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT)) // do { // 此处是一个do-while循环 // 使用p17和p9记录当前bucket的imp和sel // 同时bucket--,将bucket向前移动 1: ldp p17, p9, [x13], #-BUCKET_SIZE // {imp, sel} = *bucket-- // _cmd就是当前要调用方法的方法编号 // 比较缓存中获取到的sel与我们传入的_cmd是否一致,如果一致则调用CacheHit命中方法,结束 cmp p9, p1 // if (sel != _cmd) { // 如果不一致则调用 3:方法 b.ne 3f // scan more // } else { 2: CacheHit \Mode // hit: call or return imp // } 3: cbz p9, \MissLabelDynamic // if (sel == 0) goto Miss; // 循环结束条件 当获取的bucket地址小于buckets的首地址时,说明已经取超,跳出循环 cmp p13, p10 // } while (bucket >= buckets) // 跳转 1:方法,比较当前sel与_cmd是否一致 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 // 调出循环,则说明当前获取的bucket已经超过了buckets了 // 此时需要将bucket移动到buckets的最后,在重新从后向前查找一遍 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 // 标记已经查找到buckets一次了 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 && // 当在查找到buckets还为找到时,则结束快速查找,说明缓存中没有要调用的方法 ccmp p13, p12, #0, ne // bucket > first_probed) b.hi 4b LLookupEnd\Function: LLookupRecover\Function: b \MissLabelDynamic复制代码
快速查找完整流程图
作者:少说多笑
链接:https://juejin.cn/post/7021440509453271054