Part1主要讲了Objective-C的对象模型以及怎样动态创建一个类,稍微说了点运行时的消息传递,这一部分将要重点说一下运行时以及其中最重要的一个概念-消息,以及消息是怎么传递、转发的。

运行时

运行时是一套开源的C和汇编代码库,是Objective-C的运行系统,它能为编译好的Objective-C代码提供强大的动态特性;运行时由两套版本:lagecy versionmodern version,在64bit的 OS X 及iOS平台都是使用的modern version,当然后者的功能更强大一些。

那么怎样才能和运行时系统进行交互呢?Objective-C提供了三种方法。

  • 不知不觉的交互

运行时其实无处不在,只是有时候我们可能并没有感觉到它的存在而已;当我们向一个对象发送任意一条消息,背后就是运行时控制去完成的。

  • 间接的交互

Fondation框架NSObject对象定义了一些自省方法,这些方法都是和运行时去打交道。比如:和类的继承关系有关的一些方法isKindOfClass:,isMemberOfClass:,判断能不能响应某一个方法,是不是符合某一个协议的:respondsToSelector:conformsToProtocol:还有对象之间进行相比较的isEqual:,这些都是由运行时去完成的。

  • 直接的交互

通过导入头文件<objc/runtime.h>,我们就可以直接使用运行时来进行编程了,其方法大致一共分下面几类,和类相关的方法(只说前缀) class_,和对象相关的方法:objc_,和方法相关的:method_ sel_ imp_ ,和协议相关的:protocol_ 和属性相关的 property_

消息

在第一种交互方式中,我们发送一条消息,运行时在中间是怎么工作的呢?其过程主要分为以下几步:

  • 编译器的转换

当我们向一个对象发送一条消息,

1
2

[receiver message]

第一条指令编译器会帮我们变成这样

1
id objc_msgSend(id self, SEL _cmd, ...)
1
2
3
4
5
/// 不透明结构体, selector
typedef struct objc_selector *SEL;

/// 函数指针, 用于表示对象方法的实现
typedef id (*IMP)(id, SEL, ...);

其中,self是消息接受者,即receiver,_cmd是selector,…是可变参数,这就是一个标准的C函数,它的指针我们用IMP来表示,其实,我们向要调用的方法最终也是一个C函数,;我们向一个对象发送一条消息目的是要执行某一个方法,到了运行时那里,就变成了根据消息接收者和selector去寻找函数指针IMP,并且调用执行它。而且运行时会隐含给我们所调用的方法发送两个隐含参数self和_cmd,我们可以在方法中使用它。

当向一般对象发送消息时,调用objc_msgSend;当向super发送消息时,调用的是objc_msgSendSuper; 如果返回值是一个结构体,则会调用objc_msgSend_stret或objc_msgSendSuper_stret。

这里需要注意的一点时,super只是编译器的一个提示符,让运行时直接去调用父类的方法,其消息接收者是self;面试题:[super class]返回什么?详细细节可参考这里

  • objc_msgSend的执行过程 - 消息处理

为了加快速度,苹果对这个方法做了很多优化,这个方法是用汇编实现的。伪代码表示:

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
id objc_msgSend(id self, SEL op, ...) {
if (!self) return nil;
IMP imp = class_getMethodImplementation(self->isa, SEL op);
imp(self, op, ...); //调用这个函数,伪代码...
}

//查找IMP
IMP class_getMethodImplementation(Class cls, SEL sel) {
if (!cls || !sel) return nil;
IMP imp = lookUpImpOrNil(cls, sel);
if (!imp) return _objc_msgForward; //这个是用于消息转发的
return imp;
}

IMP lookUpImpOrNil(Class cls, SEL sel) {
if (!cls->initialize()) {
_class_initialize(cls);
}

Class curClass = cls;
IMP imp = nil;
do { //先查缓存,缓存没有时重建,仍旧没有则向父类查询
if (!curClass) break;
if (!curClass->cache) fill_cache(cls, curClass);
imp = cache_getImp(curClass, sel);
if (imp) break;
} while (curClass = curClass->superclass);

return imp;
}

objc_msgSend的动作比较清晰:首先在Class中的缓存查找imp(没缓存则初始化缓存),如果没找到,则向父类的Class查找。如果一直查找到根类仍旧没有找到,则用_objc_msgForward函数指针代替imp。最后,执行这个imp。

_objc_msgForward是用于消息转发的。这个函数的实现并没有在objc-runtime的开源代码里面,而是在Foundation框架里面实现的。

这就是消息的处理过程,官方的图如下所示:

image

消息转发

由消息的处理过程我们知道,当运行时系统通过接收者和selector去寻找函数指针,直到NSObject类都没有找着的时候,就会跳转到消息转发,如果消息转发之后依然没有处理,也没被吞掉,则程序会执行doesNotRecognizeSelector:,然后crash。在程序crash之前,程序员一共有三次机会进行手动转发,分别是:

  • 使用动态方法解析
1
+ (BOOL)resolveInstanceMethod:(SEL)selector

消息转发的第一步,当转发的是实例方法时,运行时给接收者发送上面的消息,询问开发者是否进行方法解析,此处可以通过运行时提供的

1
class_addMethod(self,selector,(IMP)autoDictionarySetter,"v@:@");

来动态为类指定的selector添加一个实现,然后并执行返回,随后会把这对selector-IMP缓存起来。如果转发的是类方法时,运行时会调用下面的方法,执行过程一样。如果没有添加实现的话,执行下一步.

1
+ (BOOL)resolveClassMethod:(SEL)sel);

如果在声明一个属性之后,我们在impenmention文件中使用了@dynamic关键字,意思是告诉编译器一不要synthesize属性,二不要报错,属性的getter/setter方法我会在消息转发时给你找到。那么这个时候,我们就可以在动态方法解析中为用@dynamic定义的属性添加setter或setter方法。

  • 使用备援接收者(快速转发)
1
- (id)forwardingTargetForSelector:(SEL)aSelector);

如果第一步没有添加实现的话(即使retune YES也白搭,运行时目前的实现只是用这个返回值来判断是否打印Log),运行时会给开发者第二次机会去用一个备援接收者去接收这个selector,然后把这个接收者返回;如果能找到的话就执行,如果没有找到,或者返回的是nil,运行时会接着执行第三步。

  • 完整的消息转发
1
2
3
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

- (void)forwardInvocation:(NSInvocation *)anInvocation;

这个过程分为两步:

  • 运行时给接收者发送消息 methodSignatureForSelector: 来获取方法签名,方法签名知识一个对方法类型的封装,包含了参数个数,参数类型,返回值类型,是否属于oneway等信息,运行时会根据这个方法签名及aSelector生成一个NSInvocation对象,然后继续朝下传递。如果返回nil,则运行时会给接收者发送doesNotRecognizeSelector:,程序会crash.

  • 运行时根据方法签名及SEL封装成NSInvoation,然后给接收者发送forwardInvocation:,这个方法可以实现得很简单:只需改变调用目标,使消息在新目标上得以调用即可。然而这样实现出来的方法与备援接收者方案所实现的方法等效,所以很少有人采用这么简单的实现方式。比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或是改换selector,等等。如果实现这个方法而不对NSinvocation作处理的话,这条消息就意味者被吞掉。

实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法。这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最后调用了NSObject类的方法,那么该方法还会继而调用doesNotRecognizeSelector:以抛出异常,此异常表明选择子最终未能得到处理。

消息转发与多重继承

在第二种情况-使用备援接收者时,就能很明显的看到多重继承的影子,如果要实现的更完整一些,可以参考下面官方给出的范例:

  • 第一步 :重写自省方法
    因为在程序中需要进行如下的判断:
1
if ( [aWarrior respondsToSelector:@selector(negotiate)] )

所以我们要重写respondsToSelector: isKindOfClass: instancesRespondToSelector: conformsToProtocol:等方法

1
2
3
4
5
6
7
8
9
10
11
- (BOOL)respondsToSelector:(SEL)aSelector {

if ( [super respondsToSelector:aSelector] )
return YES;
else {
/* Here, test whether the aSelector message can *
* be forwarded to another object and whether that *
* object can respond to it. Return YES if it can. */

}
return NO;
}
  • 第二步 : 生成方法签名
1
2
3
4
5
6
7
8
9
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector {

NSMethodSignature* signature = [super methodSignatureForSelector:selector];
if (!signature) {
signature = [surrogate methodSignatureForSelector:selector];
}

return signature;
}
  • 第三步 : 转发
1
2
3
4
5
- (void)forwardInvocation:(NSInvocation*)invocation {

[invocation invokeWithTarget:surrogate];

}

总结

运行时对消息的处理可归结为:自动寻找+三次手动转发

通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中。

对象可以把其无法解读的某些选择子转交给其他对象来处理。

经过上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制。

消息转发的原则是:越早处理越好,因为越往后成本越高。

参考资料

1.Objective-C中的消息与消息转发

2.Objective-C Runtime Programming Guide

3.Understanding the Objective-C Runtime

5.objc_msgSend() Tour Part 1: The Road Map

4.理解Objective-C Runtime

6.《Effective Objective-C 2.0:编写高质量iOS与OS X代码的52个有效方法》

7.深入分析 objc_msgSend

8.stackoverflow上的一篇QA

9.深入Objective-C的动态特性