编译时与运行时
编译时
编译时顾名思义就是正在编译的时候,就是编译器帮你把源代码翻译成机器能识别的代码。实际上只是翻译成某个中间状态的语言。
那编译时就是简单的做一些翻译工作,比如检查代码规范、语法分析之类的过程。
运行时
所谓运行时就是代码跑起来了,被装在到内存中去了,是一个动态过程,而运行时类型检查就是与前面讲的编译时类型检查不一样,不是简单的扫描代码,而是在内存中做了实际操作进行判断。
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