在iOS的开发中,调用方法可谓再常见不过的事,那么方法的本质究竟是什么?这次就让我们从这个问题入手,探索方法的本质以及方法查找流程和消息转发机制。
方法的本质
新建一个项目,代码如下:
代码
#import <Foundation/Foundation.h>
@interface DZPerson : NSObject
- (void)sleep;
@end
@implementation DZPerson
- (void)sleep{
NSLog(@"sleep");
}
@end
void go(){
NSLog(@"%s",__func__);
}
int main(int argc, char * argv[]) {
@autoreleasepool {
DZPerson *p = [DZPerson alloc];
[p sleep]; // OC方法
go(); // C函数
}
}
编译
我们通过clang命令编译clang -rewrite-objc main.m -o main.cpp
得到编译后的代码,打开main.cpp,直接拉到下面查看main函数实现:
objc_msgSend
函数,因此可以得出结论:OC方法的本质就是通过objc_msgSend
等函数来发送消息。
objc_msgSend
我们可以看到objc_msgSend里有两个参数id和SEL,id是消息接受者,SEL是方法编号。
调用对象方法
DZPerson *p = [DZPerson alloc];
[p sleep];
objc_msgSend(p, sel_registerName("sleep"));
调用类方法
objc_msgSend(objc_getClass("DZPerson"), sel_registerName("eat"));
调用父类对象方法
struct objc_super dzSuper;
dzSuper.receiver = p;
dzSuper.super_class = [DZSuper class];
objc_msgSendSuper(&dzSuper, @selector(hello));
调用父类类方法
struct objc_super myClassSuper;
myClassSuper.receiver = [p class];
myClassSuper.super_class = class_getSuperclass(objc_getClass([p class]));
objc_msgSendSuper(&myClassSuper, sel_registerName("hi"));
注意:在项目中使用objc_msgSend
函数要把校验关闭,否则会报错:
objc_msgSend源码分析
我们通过查找可以看到:
objc_msgSend
的源码很长就不展示了,建议大家自己查看。
我们可以看到objc_msgSend
的源码是使用汇编来编写的,个人认为主要原因有以下几种:
- 性能:高级代码最终会转换成汇编语言来让机器识别,直接使用汇编语言效果会更高。
- 限制:C、C++语言作为静态语言没有办法能满足个数未知、类型未知的函数。
- 安全:为了防止系统函数被hook,使用汇编来调用方法和实现函数更为安全。
流程总结
简单总结一下objc_msgSend源码流程:
- 从
ENTRY _objc_msgSend
开始。 - 对消息接受者(id self, SEL _cmd)进行判断处理。
- taggedPointer判断。
- 通过
GetClassFromIsa_p16
对isa进行处理得到class。 - 通过
CacheLookup
去查找缓存。 - 通过指针偏移找到cache_t,然后找到bucket,通过sel哈希算法之后的key去寻找imp,找到则表示缓存命中(
CacheHit
)返回imp,找不到则跳到JumpMiss
。 - 通过查看
JumpMiss
可以一路找到__objc_msgSend_uncached
->MethodTableLookup
。 MethodTableLookup
里调用bl __class_lookupMethodAndLoadCache3
来到方法的慢速查找流程。
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
至此,objc_msgSend
的汇编源码流程就分析完毕了,这其实也是方法的快速查找流程。
总结
OC方法会在编译期间被编译成objc_msgSend
等函数,然后在底层汇编代码中查找缓存中sel对应的imp,找到了就直接返回,这便是方法的快速查找流程,找不到则进入方法的慢速查找流程。