Objective-C 不同于 其他语言,它是动态的。何为动态,就是说它在执行一个方法的时候,比如[object method]并不是去直接执行,而是将method当作一条消息发送,这个消息由object来处理,这个消息可能被转发给另外一个对象。例[object init]调用[super init]或者不予理睬假装没有收到此消息比如给button 添加一个SEL.多条不同的消息也可以对应同一个方法实现。这都是在程序运行的过程时候决定的。

实际情况下,我们所使用OC调用的语法都会被解析成一个C的函数调用 - object_msgSend().例如以下代码是等价的:

1.[object method:message];
2.object_msgSend(object,@selector(method:),message)

消息传递的关键存在于objc_object 中的isa指针和objc_class中的class dispatch table.

Objc_object,Objc_class以及objc_method

在OC中,类、对象和方法都是一个C的结构体,从objc/objc.h头文件中,我们可以找到他们的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
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;
#endif
};
struct objc_method_list {
struct objc_method_list *obsolete;
int method_count;
#ifdef __LP64__
int space;
#endif
/* variable length structure */
struct objc_method method_list[1];
};
struct objc_method {
SEL method_name;
char *method_types; /* a string representing argument/return types */
IMP method_imp;
};

objc_method_list本质是一个由objc_method元素的可变长度数组.一个objc_method结构体中有函数名,也就是SEL,函数类型的字符串,以及函数的实现IMP。
从这些定义中可以看出发送一条消息也就 objc_msgSend 做了什么事。举objc_msgSend(obj, foo) 这个例子来说:

1
2
3
4
5
6
具体会转换成什么代码呢?
Runtime会根据类型自动转换成下列某一个函数:
objc_msgSend:普通的消息都会通过该函数发送
objc_msgSend_stret:消息中有数据结构作为返回值(不是简单值)时,通过此函数发送和接收返回值
objc_msgSendSuper:和objc_msgSend类似,这里把消息发送给父类的实例
objc_msgSendSuper_stret:和objc_msgSend_stret类似,这里把消息发送给父类的实例并接收返回值
  1. 首先,通过 obj 的 isa 指针找到它的 class ;
  2. 在 class 的 method list 找 foo ;
  3. 如果 class 中没到 foo,继续往它的 superclass 中找 ;
  4. 一旦找到 foo 这个函数,就去执行它的实现IMP .

当消息被发送到实例对象时,是如图所示处理的(图片源自网络):

但这种实现有个问题,效率低。但一个 class 往往只有 20% 的函数会被经常调用,可能占总调用次数的 80% 。每个消息都需要遍历一次 objc_method_list 并不合理。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。这也就是 objc_class 中另一个重要成员 objc_cache 做的事情 - 再找到 foo 之后,把 foo 的 method_name 作为 key ,method_imp 作为 value 给存起来。当再次收到 foo 消息的时候,可以直接在 cache 里找到,避免去遍历 objc_method_list.

消息动态解析

动态方法解析流程图:

在上面的例子中,如果 foo 没有找到会发生什么?通常情况下,程序会在运行时挂掉并抛出 unrecognized selector sent to … 的异常。但在异常抛出前,Objective-C 的运行时会给你三次拯救程序的机会:

  1. Method resolution
  2. Fast forwarding
  3. Normal forwarding
  • 第一步:通过+resolveInstanceMethod:或者 +resolveClassMethod:,方法决定是否动态添加方法。如果返回Yes则通过class_addMethod动态添加方法,消息得到处理,结束;如果返回No,则进入下一步;
1
2
3
4
5
6
7
8
9
10
11
12
13
void fooMethod(id obj, SEL _cmd)
{
NSLog(@"Doing foo");
}
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if(aSEL == @selector(foo:)){
class_addMethod([self class], aSEL, (IMP)fooMethod, "v@:");
return YES;
}
return [super resolveInstanceMethod];
}

iOS 4.3加入runtime 方法,主要都是以 imp 为前缀的方法,比如 imp_implementationWithBlock() 用 block 快速创建一个 imp 。
上面的例子可以重写成:

1
2
3
4
5
IMP fooIMP = imp_implementationWithBlock(^(id _self) {
NSLog(@"Doing foo");
});
class_addMethod([self class], aSEL, fooIMP, "v@:");
  • 第二步:这步会进入forwardingTargetForSelector:方法,用于指定响应这个selector的备选对象,不能指定为self。如果返回某个对象则会调用对象的方法,结束。如果返回nil,则进入第三部;
1
2
3
4
5
6
7
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(foo:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}

只要这个方法返回的不是 nil 和 self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续 Normal Fowarding

  • 第三部:这步我们要通过methodSignatureForSelector:方法签名,如果返回nil,则消息无法处理。如果返回methodSignature,则进入下一步;
  • 第四部:这步调用forwardInvocation:方法,我们可以通过anInvocation对象做很多处理,比如修改实现方法,修改响应对象等,如果方法调用成功,则结束。如果失败,则进入doesNotRecognizeSelector方法,若我们没有实现这个方法,那么就会crash。
1
2
3
4
5
6
7
8
9
10
11
- (void)forwardInvocation:(NSInvocation *)invocation
{
SEL sel = invocation.selector;
if([alternateObject respondsToSelector:sel]) {
[invocation invokeWithTarget:alternateObject];
}
else {
[self doesNotRecognizeSelector:sel];
}
}

总结

Objective-C 中给一个对象发送消息会经过以下几个步骤:

  1. 在对象类的 dispatch table 中尝试找到该消息。如果找到了,跳到相应的函数IMP去执行实现代码;
  2. 如果没有找到,Runtime 会发送 +resolveInstanceMethod: 或者 +resolveClassMethod:尝试去 resolve 这个消息;
  3. 如果 resolve 方法返回 NO,Runtime 就发送 -forwardingTargetForSelector: 允许你把这个消息转发给另一个对象;
  4. 如果没有新的目标对象返回, Runtime 就会发送 -methodSignatureForSelector:-forwardInvocation: 消息。你可以发送 -invokeWithTarget: 消息来手动转发消息或者发送 -doesNotRecognizeSelector: 抛出异常。