objc_msgSend消息发送机制
一、消息发送
我们先来了解一下什么是消息发送;C语言是静态,OC是动态类型。在编译的时候不知道具体类型,运行的时候才会检查数据类型,根据函数名找到实现。实现语言动态的就是Runtime的API,主要有两大核心:
- 动态配置:动态的修改类的信息。添加属性、方法、甚至成员变量的值等数据结构。
- 消息传递:包括发送和转发。在编译的时候,方法调用会转化为objc_msgSend函数进行消息发送,即通过sel(方法名)找imp(方法实现)的过程。
二、objc_msgSend
我们通过一个demo来开始探索:
#import <Cocoa/Cocoa.h> #import <objc/message.h> @interface LGPerson : NSObject - (void)study; - (void)happy; + (void)eat; @end @implementation LGPerson - (void)study:(NSString *)arg { NSLog(@"%s",__func__); } - (void)happy { NSLog(@"%s",__func__); } + (void)eat { NSLog(@"%s",__func__); } @end int main(int argc, const char * argv[]) { @autoreleasepool { LGPerson *p = [LGPerson alloc]; [p study:@"A"]; [p happy]; } return NSApplicationMain(argc, argv); }
打开终端,在项目对应的目录下输入clang -rewrite-objc main.m将这个类转化为cpp文件,并找到其中关于main函数的部分,可以发现编译后的方法都是通过objc_msgSend发送的,这也证明了我们上面的想法,即方法的本质就是消息发送。
LGPerson *p = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc")); ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)p, sel_registerName("study:"), (NSString *)&__NSConstantStringImpl__var_folders_64_v4jdthx95753k1gbfyy30w0w0000gn_T_main_64f0fb_mi_3); ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("happy"));
同时,我们可以看到objc_msgSend带有默认的2个参数:消息的接收者id类型,消息的方法名SEl类型。alloc方法给类对象发消息,如果消息接收者是实例对象,那么实例对象就会通过isa指针找到类对象,从中找到实例方法。(类方法同理,在元类对象中找到)。如果方法带有参数,那么参数会跟在末尾。
此外,我们还在main.cpp文件中发现了objc_msgSend 家族,这些方法代表了发送给当前类对象,父类对象,等等。这也恰恰说明了为什么苹果要设计元类,就是为了objc_msgSend的复用。
三、objc_msgSendSuper
-(instancetype)init { if (self = [super init]) { NSLog(@"%@",[self class]); NSLog(@"%@",[super class]); } return self; }
我们对上面的代码进行编译发现:发送到对象超类的消息(使用super关键字)使用objc_msgSendSuper发送。接着我们从OC源码中看一下objc_msgSendSuper结构体的实现:结构体中的super_class等于父类,代表从父类对象开始查找。
我们来看一下objc_msgSend 和objc_msgSendSuper的区别
- objc_msgSend的第一个参数是self(消息的接收者),第二个参数是消息的方法名字(sel)。objc_msgSendSuper的第一个参数是__rw_objc_super类型的结构体,结构体包含两个参数:第一个参数是self(消息的接收者),第二个参数是消息的方法名字(sel)
- objc_msgSend是给本类发消息,objc_msgSendSuper是给父类发消息,结果相同,但出发点不同。所以在使用[self class] 和 [super class] 打印出来的都是self,即这个对象指向的类,因为消息的接收者不会发生改变。
四、快速查找
我们进入arm64下,看一下快速查找的过程,可以看到在其底层用到了汇编语言,这样的话可以直接使用参数,免去大量参数的拷贝开销。
五、慢速查找
我们在oc源码中查找_obcj_msgSend_uncached的入口,这是静态的STATIC_ENTRY。
进入MethodTableLookup内部:
然后,我们在进去_loopUpImpOrForward,因此我们来找一下loopUpImpOrForward:
在这个_loopUpImpOrForward,首先定义了一个消息的转发forWard_imp , 接着判断类的初始化、加锁、检查是否已知的类等,我们具体来看一下其中的for循环过程:
// unreasonableClassCount()表示循环的上限; for (unsigned attempts = unreasonableClassCount();;) { if (curClass->cache.isConstantOptimizedCache(/* strict */true)) { #if CONFIG_USE_PREOPT_CACHES imp = cache_getImp(curClass, sel); if (imp) goto done_unlock; curClass = curClass->cache.preoptFallbackClass(); #endif } else { // curClass method list. method_t *meth = getMethodNoSuper_nolock(curClass, sel); if (meth) { imp = meth->imp(false); goto done; } if (slowpath((curClass = curClass->getSuperclass()) == nil)) { // No implementation found, and method resolver didn't help. // Use forwarding. imp = forward_imp; break; } } // Halt if there is a cycle in the superclass chain. if (slowpath(--attempts == 0)) { _objc_fatal("Memory corruption in class list."); } // Superclass cache. imp = cache_getImp(curClass, sel); if (slowpath(imp == forward_imp)) { // Found a forward:: entry in a superclass. // Stop searching, but don't cache yet; call method // resolver for this class first. break; } if (fastpath(imp)) { // Found the method in a superclass. Cache it in this class. goto done; } }
第一个if判断,是再次从从cache里找,防止多线程操作时,刚好调用函数,缓存进入,我们着重看一下else中的getMethodNoSuper_nolock:
接着跳转search_method_list_inline:
跳转findMethodInSortedMethodList:
接着跳转findMethodInSortedMethodList:
进行二分查找,其中probe--,退出时候得到列表里第一次出现的地方,这是意思就是分类优先,因为分类同名的方法会排在列表靠前。多个分类有同名方法时,确保后编译的先调用。
待查找完成后,会进入go done,并跳转log_and_fill_cache:
将方法插入到类的方法缓存中。
六、总结
1.方法的本质就是消息发送
消息的发送在编译的时候,编译器就会把方法转换为objc_msgSend这个函数。函数通过消息的接收者和方法名找到具体的实现。接收者是实例对象,通过isa找到类对象,再通过方法名在类对象的方法缓存中找到实现。如果接收者是类对象,就在元类里找。
2.objc_msgSendSuper 和 [super class] 在调用父类方法只是出发点不一样,但是结果是一样的。
使用super
关键字调用父类方法,消息会通过objc_msgSendSuper发送。 super
和self
调用方法的区别就在于,查找方法的时候出发点不一样。self
会从当前类开始找,而super
会从当前类的父类。
3.消息的快速查找流程
- 判断receiver(消息的接受者)是否存在
- receiver 通过isa 找到 class
- class 首地址通过内存平移得到缓存cache
- cache中获取buckets容器
- 遍历buckets容器,与元素比对方法名(元素是bucket_t结构体类型),包含_sel和——imp成员变量
- 如果找到相等的就执行CacheHit方法,调用imp
- 如果没有,执行_objc_msgSend_uncached,进入慢速查找
4.消息的慢速查找流程
- 开始lookUpImpOrForward,再次从cahe里查找,因为多线程可能已经缓存进来了
- 先从当前类的methodList开始查找,已排序的用二分查找,未排序的用线性查找
- 如果没有就找父类的cache
- 再找父类的methodList
- 如果父类为nil,就开始重复第二步直到父类均为nil为止