Objective-C runtime
在了解objc_msgSend
时。我们需要先了解Objective-C的runtime
机制
runtime简介
在了解runtime
之前,先区分个概念编译时
与运行时
。
编译时
:顾名思义,就是正在编译的时候,编译器帮你把源代码翻译成机器能识别的代码
。(当然只是一般意义上这么说,实际上可能只是翻译成某个中间状态的语言)。在这个过程中,编译器不关心你的具体流程,只关心你是否有语义
或语句
上的错误。运行时
:就是代码跑起来了,已经被装载到内存中调用
了。(在应用没有运行时,你的代码或应用是保存在磁盘上的,是个死的。只有跑到内存中才会被激活)。而运行时类型检查
就是与前面讲的编译时类型检查
(或者静态类型检查)不一样,不是简单的扫描代码。而是在内存
中做些操作
,做些判断
。
runtime版本
Runtime有两个版本:一个Legacy版本(早期版本),一个Modern版本(现行版本)
-
早期版本对应的编程接口:Objective-C 1.0
-
现行版本对应的编程接口:Objective-C 2.0
-
早期版本用于Objective-C 1.0、32位的Mac OS X的平台上
-
香型版本用于iOS和Mac OS X v10.5及以后的系统中的64位程序
Objective-C Runtime Programming Guide
runtime 交互的三种方式
- Objective-C Code 直接调用
比如直接调用方法[self addSubview:]
、@selector()
等 - Framework&Service
比如NSSelectorFromString
、isKindOfClass
、isMemberOfClass
等方法 - Runtime API
比如sel_registerName
、class_getInstanceSize
等底层方法。
Objective-C方法本质
- 新建一个LGPerson类,并声明一个实例方法。
@interface LGPerson : NSObject
- (void)sayHello;
- (void)sayNB;
@end
- 在main.h文件中,实例化LGPerson,并调用sayHello方法。
LGPerson *person = [LGPerson alloc];
[person sayHello];
- 执行Clang,将main.m文件编译成main.cpp文件,并找到main函数
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hr_l_56yp8j4y11491njzqx6f880000gn_T_main_93c270_mi_1);
}
return 0;
}
解析
- ** 分析main.cpp文件中的main函数**
- 在调用alloc、sayHello时,都调用了objc_msgSend方法,
- objc_getClass(“LGPerson”)获取LGPerson类
- sel_registerName(“sayHello”),获取调用方法,类似于==@selector==、NSSelectorFromString()。
- **修改工程中的Apple Clang - Preprocessing配置
- **使用objc_msgSend方式调用sayHello方法
LGPerson *person = [LGPerson alloc];
[person sayHello];
objc_msgSend(person, sel_registerName("sayHello"));
打印结果:objc_msgSend成功调用了sayHello方法。
- 调用父类方法
- 新建LGTeacher类,并继承于LGPerson类
@interface LGTeacher : LGPerson
- (void)sayNB;
@end
- 在main.m文件中,实例化LGTeacher,并调用sayHello方法;使用objc_msgSendSuper调用sayHello方法。
LGTeacher *teacher = [LGTeacher alloc];
[teacher sayHello];
struct objc_super lgsuper;
lgsuper.receiver = teacher;
lgsuper.super_class = [LGPerson class];
objc_msgSendSuper(&lgsuper, sel_registerName("sayHello"));
** 打印结果**:两种方式都调用了父类LGPerson的sayHello方法
总结
- 调用方法的本质:发送消息。
- Objective-C调用方法等价于runtime中的objc_msgSend和objc_msgSendSuper消息发送。
思考:objc_msgSend是怎样找到对应的方法呢?即sel如何找到对应的imp?
objc_msgSend介绍
在objc源码中,我们会发现objc_msgSend是使用汇编实现的,汇编的主要特性是:
- 速度快:汇编更容易被机器识别。
- 方法参数的动态性:汇编调用函数时传递的参数是不确定的,那么发送消息时,直接调用一个函数就可以发送所有的消息。
消息查找机制
- 快速查找:cache中查找
- 慢速查找:
- methodList中查找
- 消息转发
objc_msgSend分析
快速查找:cache_t中查找
源码分析
objc_msgSend调用
objc_msgSend(person, sel_registerName("sayHello"));
objc_msgSend传入两个参数:分别为消息接收者和sel。
objc_msgSend源码
- 在objc4-781中,在objc-msg-arm64.s文件找到ENTRY _objc_msgSend部分,注:在汇编中的方法会比在OC的方法多一个_.
ENTRY _objc_msgSend
UNWIND _objc_msgSend, NoFrame
// p0 是传入的第一个参数,即消息的接收者
cmp p0, #0 // nil check and tagged pointer check,判断p0是否为空
#if SUPPORT_TAGGED_POINTERS
// 小对象类型
b.le LNilOrTagged // (MSB tagged pointer looks negative)
#else
// p0位空时,执行LReturnZero,即返回空
b.eq LReturnZero
#endif
// p13 获取消息接收者的isa
ldr p13, [x0] // p13 = isa
// p16 根据isa 获取到Class
GetClassFromIsa_p16 p13 // p16 = class
LGetIsaDone:
// calls imp or objc_msgSend_uncached
// 在cache中开始查找imp
CacheLookup NORMAL, _objc_msgSend
CacheLookUp源码 缓存中查找imp
- 在objc-msg-arm64.s文件中找到==.macro CacheLookup==
.macro CacheLookup
LLookupStart$1:
// p1 = SEL, p16 = isa
// isa 平移16字节得到 cache_t,cache首地址是mas_bukets
ldr p11, [x16, #CACHE] // p11 = mask|buckets
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
// 获取buckets p11 & 0x0000ffffffffffff 得到后48位 buckets
and p10, p11, #0x0000ffffffffffff // p10 = buckets
// 获取hash 搜索下标:逻辑右移48位 得到mask;然后p1 & mask给p12 得到hash存储的key
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
// p12是获取到的下标,然后逻辑左移4位,再由p10(buckets)平移,得到对应的bucket保存到p12中
add p12, p10, p12, LSL #(1+PTRSHIFT)
// p12 = buckets + ((_cmd & mask) << (1+PTRSHIFT))
// 将p12属性imp和sel分别赋值为p17 和 p9
ldp p17, p9, [x12] // {imp, sel} = *bucket
// 判断当前bucket的sel和传入的sel是否相等
1: cmp p9, p1 // if (bucket->sel != _cmd)
// 如果不相同,则跳入2
b.ne 2f // scan more
// 如果相同直接返回imp
CacheHit $0 // call or return imp
2: // not hit: p12 = not-hit bucket
// 如果没有找到,进入2
CheckMiss $0 // miss if bucket->sel == 0
// 如果p12 == p10,说明p12指针已经到了buckets的首地址了。
cmp p12, p10 // wrap if bucket == buckets
// 如果相等,跳入3
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
// 再将p12的指针指到buckets的最后一个元素
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.
#endif
// Clone scanning loop to miss instead of hang when cache is corrupt.
// The slow path may detect any corruption and halt later.
// 然后继续查找,直到找到或者再次 bucket与buckets再次相等,跳出循环。
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