OC的Runtime机制之消息
ObjC的方法调用都是动态的,这点和其他的语言是有区别的,为了更深层次理解动态的概念,我们必须先知道 Class,SEL,IMP这三个概念。
typedef struct objc_class *Class; typedef struct objc_object {
Class isa; } *id;
typedef struct objc_selector *SEL; typedef id (*IMP)(id, SEL, ...);
class 的含义我们之前讲个,他被定义为一个指向obic_class的结构体指针,这个结构体的定义如下
struct objc_class {
struct objc_class* isa;struct objc_class super_class; /*父类*/
const char *name; /*类名字*/ long version; /*版本信息*/
long info; /*类信息*/long instance_size; /*实例大小*/struct objc_ivar_list *ivars; /*实例参数链表*/
struct objc_method_list **methodLists; /*方法链表*/
struct objc_cache *cache; /*方法缓存*/
struct objc_protocol_list *protocols; /*协议链表*/
}
有此可见,Class是指向类结构体的指针,改结构体含有一个指向其父类类结构的指针,该类的名称,实例大小等其他信息。
NSObject 的 class 方法就返回这样一个指向其类结构的指针。每一个类实例对象的第一个实例变量是一 个指向该对象的类结构指针,叫做isa,通过isa,对象可以访问它对应的类,以及父类。实例对象的第一个实例变量为 isa,它指向该类的类结构 The object’s class。 而该类结构有一个指向其父类类结构的指针 superclass, 以及自身消息名称(selector)/实现地址(address) 的方法链表。如下图
方法的含义:
这里所说的方法链表里面储存的是Method类型的。图中selector就是指Method的SEL,address就是address 就是指 Method 的 IMP 。
Method 的结构如下
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
一个方法 Method,其包含一个方法选标 SEL – 表示该方法的名称,一个 types – 表示该方法参数的类型, 一个 IMP - 指向该方法的具体实现的函数指针。
SEL的含义:
SEL的定义为:typedef struct objc_selector *SEL;
不同的类可以拥有相同的 selector,这个没有问题,因为不同类的实例对象 performSelector 相同的 selector 时,会在各自的消息选标(selector)/实现地址(address) 方法链表中根据 selector 去查找具体的 方法实现 IMP, 然后用这个方法实现去执行具体的实现代码。这是一个动态绑定的过程,在编译的时候, 我们不知道最终会执行哪一些代码,只有在执行的时候,通过 selector 去查询,我们才能确定具体的执行 代码。
方法列表的定义
//方法列表
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
}
IMP 的含义:在前面我们也看到 IMP 的定义为: typedef id (*IMP)(id, SEL, ...);
IMP 是一个函数指针,这个被指向的函数包含一个接收消息的 对象 id(self 指针), 调用方法的选标 SEL (方法名),以及不定个数的方法参数,并返回一个 id。也就是说 IMP 是消息最终调用的执行代码,是方法真正的实现代码 。我们可以像在C语言里面一样使用这个函数 指针。
NSObject 类中的 methodForSelector:方法就是这样一个获取指向方法实现 IMP 的指针, methodForSelector:返回的指针和赋值的变量类型必须完全一致,包括方法的参数类型和返回值类型。
使用 methodForSelector:来避免动态绑定将减少大部分消息的开销,但是这只有在指定的消息被重复发 送很多次时才有意义,例如上面的 for 循环。
注意,methodForSelector:是 Cocoa 运行时系统的提供的功能,而不是 Objective-C 语言本身的功能。
下面我们总结一下:
1.一个实例对象的第一个实例变量是isa,这个isa指向该对象的类结构,类结构有指向父类类结构。
2.在元类中是储存类方法的,方法链表里储存的是Mehtod类型的。SEL代表改方法的名称,IMP代表该方法具体实现的函数指针。
3.当实例对象查找方法时,会在方法列表中根据SEL去查找具体的实现的IMP,根据IMP这个指针然后找到具体实现的代码,这个是一个动态绑定的过程。
消息调用过程:
先举出一个例子
Cat * cat = [[Cat alloc] init];
[cat eat];
消息函数 obj_msgSend:当调用方法的时候,编译器会将消息转换为objc_msgSend 的调用,两个参数,消息接收者id和消息对应的方法名称SEL,同时接收消息的任何参数。
id objc_msgSend(id theReceiver, SELtheSelector, ...)
如上:
objc_msgSend(cat,@selector(eat) );
该消息做了动态绑定的所需要的一切工作:
1,它首先找到 SEL 对应的方法实现 IMP。因为不同的类对同一方法可能会有不同的实现,所以找到的 方法实现依赖于消息接收者的类型。2, 然后将消息接收者对象(指向消息接收者对象的指针)以及方法中指定的参数传递给方法实现 IMP。
3, 最后,将方法实现的返回值作为该函数的返回值返回。
查找IMP的过程
我们在前面的类结构中也看到有一个叫 objc_cache *cache 的成员,这个缓存为提高效率而存在 的。每个类都有一个独立的缓存,同时包括继承的方法和在该类中定义的方法。。
查找过程:
当调用[cat eat]; 这个方法的时候,编译器会将其转换成objc_msgSend(cat,@selector(eat) );这个方法做了动态绑定的一切工作
1,首先根据SEL去该类的方法 cache 中查找,如果找到了调到6;
2,如果没有找到,就去该类的方法列表中查找。如果在该类的方法列表中找到了,跳到6,并将 它加入 cache 中缓存起来。根据最近使用原则,这个方法再次调用的可能性很大,缓存起来可以节省下次 调用再次查找的开销。
3,如果在该类的方法列表中没找到对应的 IMP,在通过该类结构中的 super_class 指针在其父类结构的方法列表中去查找,直到在某个父类的方法列表中找到对应的 IMP,返回它,并加入 cache 中;
4,如果在自身以及所有父类的方法列表中都没有找到对应的 IMP,则看是不是可以进行动态方法决议
5,如果动态方法决议没能解决问题,进入消息转发流程。
6,它首先找到 SEL 对应的方法实现 IMP。因为不同的类对同一方法可能会有不同的实现,所以找到的 方法实现依赖于消息接收者的类型。7, 然后将消息接收者对象(指向消息接收者对象的指针)以及方法中指定的参数传递给方法实现 IMP。
8, 最后,将方法实现的返回值作为该函数的返回值返回。
我们可以通过 NSObject 的一些方法获取运行时信息或动态执行一些消息:
class 返回对象的类;isKindOfClass 和 isMemberOfClass 检查对象是否在指定的类继承体系中;
respondsToSelector 检查对象能否指定相应的消息;conformsToProtocol 检查对象是否实现了指定协议类的方法;
methodForSelector 返回指定方法实现的地址。
performSelector:withObject 执行 SEL 所指代的方法。
消息转发:
通常,给一个对象发送它不能处理的消息会得到出错提示,然而,Objective-C 运行时系统在抛出错误之前, 会给消息接收对象发送一条特别的消息 forwardInvocation 来通知该对象,该消息的唯一参数是个 NSInvocation 类型的对象——该对象封装了原始的消息和消息的参数。
我们可以实现 forwardInvocation:方法来对不能处理的消息做一些默认的处理,也可以将消息转发给其他对 象来处理,而不抛出错误。
forwardInvocation:消息给这个问题提供了一个更特别的,动态的解决方案:当一个对象由于没有相应的方 法实现而无法响应某消息时,运行时系统将通过 forwardInvocation:消息通知该对象。每个对象都从 NSObject 类中继承了 forwardInvocation:方法。然而,NSObject 中的方法实现只是简单地调用了 doesNotRecognizeSelector:。通过实现我们自己的 forwardInvocation:方法,我们可以在该方法实现中将 消息转发给其它对象。
要转发消息给其它对象,forwardInvocation:方法所必须做的有:
1,决定将消息转发给谁,并且 2,将消息和原来的参数一块转发出去。
- (void) forwardInvocation:(NSInvocation *)anInvocation {
if ([someOtherObject respondsToSelector:[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else[super forwardInvocation:anInvocation];
}
转发消息后的返回值将返回给原来的消息发送者。您可以将返回任何类型的返回值,包括: id,结构体,浮 点数等。
forwardInvocation:方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者它也 可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消息,或者 简单的"吃掉―某些消息,因此没有响应也没有错误。forwardInvocation:方法也可以对不同的消息提供同样 的响应,这一切都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。
注意: forwardInvocation:方法只有在消息接收对象中无法正常响应消息时才会被调用。 所以,如果我们 希望一个对象将 negotiate 消息转发给其它对象,则这个对象不能有 negotiate 方法,也不能在动态方法 决议过程中为之提供实现。否则,forwardInvocation:将不可能会被调用。