Objective-C runtime之消息(二)
一、runtime中的消息
1、什么是消息
进入今天的正题之前,先来说说跟message息息相关的几个概念
①message(消息)
message的具体定义很难说,因为并没有真正的代码描述,简单的讲message 是一种抽象,包括了函数名+参数列表,他并没有实际的实体存在。
②method(方法)
method是真正的存在的代码。如:- (int)meaning { return 42; }
③selector(方法选择器)
selector 通过SEL类型存在,描述一个特定的method 或者说 message。在实际编程中,可以通过selector进行检索方法等操作。
2、两个跟消息相关的概念
①SEL
SEL又叫方法选择器,这到底是个什么玩意呢?在objc.h中是这样定义的:
typedef struct objc_selector *SEL;这个SEL表示什么?首先,说白了,方法选择器仅仅是一个char *指针,仅仅表示它所代表的方法名字罢了,有如下证据:
SEL selector = @selector(message); //@selector不是函数调用,只是给这个坑爹的编译器的一个提示 NSLog (@"%s", (char *)selector); //print message这时打印的结果就是:message
-(void)setWidth:(int)width; -(void)setWidth:(double)width;这样的函数则被认为是一种编译错误,而这最终导致了一个非常非常奇怪的Objective-C特色的函数命名:
-(void)setWidthIntValue:(int)width; -(void)setWidthDoubleValue:(double)width;可能有人会问,runtime费了那么老半天劲,究竟想做什么?GC来了。
到这里,我们明白了,本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度!!!!
②IMPtypedef id (*IMP)(id, SEL, ...);这个比SEL要好理解多了,熟悉C语言的同学都知道,这其实是一个函数指针。前面介绍过的SEL,就是为IMP服务的。由于每个方法都对应唯一的SEL,因此 我们可以通过SEL方便、快速、准确的获得它所对应的IMP(也就是函数指针),而在取得了函数指针之后,也就意味着我们取得了执行的时候的这段方法的代码的入口,这样我们就可以像普通的C语言函数调用一样使用这个函数指针。当然我们可以把函数指针作为参数传递到其他的方法,或者实例变量里面,从而获得极大的动态性。
void (* performMessage)(id,SEL);//定义一个IMP(函数指针) performMessage = (void (*)(id,SEL))[self methodForSelector:@selector(message)];//通过methodForSelector方法根据SEL获取对应的函数指针 performMessage(self,@selector(message));//通过取到的IMP(函数指针)跳过runtime消息传递机制,直接执行message方法
用IMP 的方式,省去了runtime消息传递过程中所做的一系列动作,比直接向对象发送消息高效一些。
[receiver message]在编译后会变成:
objc_msgSend(receiver, selector)实际上,同objc_msgSend方法类似的还有几个:
objc_msgSend_stret(返回值是结构体) objc_msgSend_fpret(返回值是浮点型) objc_msgSendSuper(调用父类方法) objc_msgSendSuper_stret(调用父类方法,返回值是结构体)它们的作用都是类似的,为了简单起见,后续介绍消息和消息传递机制都以objc_msgSend方法为例。
二、消息调用流程
一切还是从消息表达式[receiver message]开始,在被转换成objc_msgSend(receiver, SEL)后,在运行时,runtime system会做以下事情:
1、检查忽略的Selector,比如当我们运行在有垃圾回收机制的环境中,将会忽略retain和release消息。
2、检查receiver是否为nil。不像其他语言,nil在objective-C中是完全合法的,并且这里有很多原因你也愿意这样,比如,至少我们省去了给一个对象发送消息前检查对象是否为空的操作。如果receiver为空,则会将 selector也设置为空,并且直接返回到消息调用的地方。如果对象非空,就继续下一步。
3、接下来会根据SEL到当前类中查找对应的IMP,首先会在cache中检索它,如果找到了就根据函数指针跳转到这个函数执行,否则进行下一步。
4、检索当前类对象中的方法表(method list),如果找到了,加入cache中,并且就跳转到这个函数之行,否则进行下一步。
5、从父类中寻找,直到根类:NSObject类。找到了就将方法加入对应类的cache表中,如果仍为找到,则要进入后文介绍的内容:动态方法决议。
6、如果动态方法决议仍不能解决问题,只能进行最后一次尝试,进入消息转发流程。
7、如果还不行,去死吧。
下面的图部分展示了这个调用过程:
写到这大家肯定会发出这样的疑问:我仅仅想调用一个方法而已,却不得不经历那么多步骤,效率上怎么保证??苹果也做了一些优化上的工作。
三、函数检索优化措施
主要从下面两个方面着手:
1、通过SEL进行IMP匹配
先来看看类对象中保存的方法列表和方法的数据结构:
typedef struct method_list_t { uint32_t entsize_NEVER_USE; uint32_t count; struct method_t first; } method_list_t; typedef struct method_t { SEL name; const char *types;//参数类型和返回值类型 IMP imp; } method_t;在前一篇文章介绍SEL的时候,我们已经说过了苹果在通过SEL检索IMP时做的努力,这里不再累述。
2、cache缓存
cache的原则就是缓存那些可能要执行的函数地址,那么下次调用的时候,速度就可以快速很多。这个和CPU的各种缓存原理相通。好吧,说了这么多了,再来认识几个名词:
struct objc_cache { uintptr_t mask; uintptr_t occupied; cache_entry *buckets[1]; }; typedef struct { SEL name; void *unused; IMP imp; } cache_entry;看这个结构,有没有搞错又是hash table。
如果在缓存中已经有了需要的方法选标,则消息仅仅比函数调用慢一点点。如果程序运行了足够长的时间,几乎每个消息都能在缓存中找到方法实现。程序运行时,缓存也将随着新的消息的增加而增加。据牛人说(没有亲测过),苹果通过这些优化,使消息传递和直接的函数调用效率上的差距已经相当的小。
四、方法调用中的隐藏参数
亲爱的Objective-C程序员们,你们在进行面向对象编程的时候,在实例方法中都是用过self关键字吧,可是你有没有想过,为什么在一个实例方法中,通过self关键字就能取到调用当前方法的对象呢?这就要归功与runtime system消息的隐藏参数了。(注:在此修正,类方法和实例方法中,都可以访问self和_cmd这两个属性,因为它们都不属于类的实例变量,而是形参!!!!误导大家了,深表歉意!!!!)
当objc_msgSend找到方法对应的实现时,它将直接调用该方法实现,并将消息中所有的参数都传递给方法实现,同时,它还将传递两个隐藏的参数:
接收消息的对象(也就是self指向的内容)
方法选标(_cmd指向的内容)
这些参数帮助方法实现获得了消息表达式的信息。它们被认为是”隐藏“的是因为它们并没有在定义方法的源代码中声明,而是在代码编译时是插入方法的实现中的。尽管这些参数没有被显示声明,但在源代码中仍然可以引用它们(就象可以引用消息接收者对象的实例变 量一样)。在方法中可以通过 self 来引用消息接收者对象,通过选标_cmd 来引用方法本身。下面的例子很好的说明了这个问题:
- (void)message { self.name = @"James";//通过self关键字给当前对象的属性赋值 SEL currentSel = _cmd;//通过_cmd关键字取到当前函数对应的SEL NSLog(@"currentSel is :%s",(char *)currentSel); }打印结果:
ObjcRunTime[693:403] currentSel is :message当然,在这两个参数中,self 更有用,更常用一些。实际上,它是在方法实现中访问消息接收者对象的实例变量的途径。
外面的风吹的太猛了点,北京的天气又是雾霾又是大风,真受不鸟
-------------未完待续-------------