编译时与运行时
编译时
编译时顾名思义就是正在编译的时候,就是编译器帮你把源代码翻译成机器能识别的代码。实际上只是翻译成某个中间状态的语言。
那编译时就是简单的做一些翻译工作,比如检查代码规范、语法分析之类的过程。
运行时
所谓运行时就是代码跑起来了,被装在到内存中去了,是一个动态过程,而运行时类型检查就是与前面讲的编译时类型检查不一样,不是简单的扫描代码,而是在内存中做了实际操作进行判断。
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@endint 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汇编源码
```objectivec 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
<a name="Ta2XC"></a>### 流程图<a name="tiRoe"></a>### CacheLookup源码```objectivecLLookupStart\Function:// p1 = SEL, p16 = isa#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRSldr p10, [x16, #CACHE] // p10 = mask|bucketslsr p11, p10, #48 // p11 = maskand p10, p10, #0xffffffffffff // p10 = bucketsand w12, w1, w11 // x12 = _cmd & mask#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16// 通过isa内存平移16位,获取cache首地址,cache首地址就是maskAndBucketsldr p11, [x16, #CACHE] // p11 = mask|buckets#if CONFIG_USE_PREOPT_CACHES#if __has_feature(ptrauth_calls)tbnz p11, #0, LLookupPreopt\Functionand p10, p11, #0x0000ffffffffffff // p10 = buckets#elseand p10, p11, #0x0000fffffffffffe // p10 = bucketstbnz p11, #0, LLookupPreopt\Function#endifeor p12, p1, p1, LSR #7and p12, p12, p11, LSR #48 // x12 = (_cmd ^ (_cmd >> 7)) & mask#else// maskAndBuckets是一个共用体,共占8位64字节 高16字节存储着mask值低48字节存储着buckets信息// 所以此处将mask|buckets & 0x0000ffffffffffff 获取低48字节的信息,也就是获取buckets并赋值给p10and 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_4ldr p11, [x16, #CACHE] // p11 = mask|bucketsand p10, p11, #~0xf // p10 = bucketsand p11, p11, #0xf // p11 = maskShiftmov p12, #0xfffflsr p11, p12, p11 // p11 = mask = 0xffff >> p11and 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就是我们当前从缓存中找到的bucketadd 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_ADDRSadd 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_4add 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 hitcmp p9, #0 // } while (sel != 0 &&// 当在查找到buckets还为找到时,则结束快速查找,说明缓存中没有要调用的方法ccmp p13, p12, #0, ne // bucket > first_probed)b.hi 4bLLookupEnd\Function:LLookupRecover\Function:b \MissLabelDynamic
快速查找完整流程图

