文章目录
  1. 1. 消息的一些概念
    1. 1.1. objc_msgSend
    2. 1.2. id
    3. 1.3. SEL
    4. 1.4. Method
    5. 1.5. IMP
  2. 2. 方法调用流程
  3. 3. 动态解析
    1. 3.1. 实现动态解析
    2. 3.2. 类型编码
  4. 4. 重定向
  5. 5. 转发
    1. 5.1. 实现转发
    2. 5.2. 转发和多继承
    3. 5.3. 完整的转发流程
  6. 6. 参考

在Objective-C中,消息直到运行时才绑定到方法实现上。编译器会将消息表达式[receiver message]转化为一个消息函数的调用,即objc_msgSend。这个函数将消息接收者和方法名作为其基础参数:

1
2
3
objc_msgSend(receiver, selector)
//参数
objc_msgSend(receiver, selector, arg1, arg2, ...)

查看源码可以知道这两个函数都是用汇编实现的。

消息的一些概念

objc_msgSend

objc_msgSend完成了消息动态转发的所有事情

  • 首先它找到selector对应的方法实现。因为同一个方法可能在不同的类中有不同的实现,所以我们需要依赖于接收者的类来找到的确切的实现
  • 它调用方法实现,并将接收者对象及方法的所有参数传给它。
  • 最后,它将实现返回的值作为它自己的返回值。

id

objc_msgSend的第一个参数receiver的类型是id,id其实是一个对象的指针

1
2
3
4
5
6
7
8
9
10
typedef struct objc_object *id;

struct objc_object {
private:
isa_t isa;
public:
// ISA() assumes this is NOT a tagged pointer object
Class ISA();
// ...
}

对象中的isa指针指向所属的类(并不一定指向所属的类),正是通过isa顺藤摸瓜找到方法的实现

SEL

SEL的定义是这样的

1
2
/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

这里等objc_selector是一个不透明的结构体,苹果并没有给出实现源码。可以用 Objc 编译器命令@selector()或者 Runtime 系统的sel_registerName函数来获得一个SEL类型的方法选择器。

Method

Method是指向method_t的指针,

1
2
3
4
5
6
7
8
9
typedef struct method_t *Method;

// in objc-runtime-new.h
struct method_t {
SEL name;
const char *types;
IMP imp;
// ...
};
  • name 方法名类型是SEL
  • types 是个char指针,保存了方法的返回值类型和参数类型,例如"@16@0:8"这是一个Getter方法的types
  • imp 方法的实现,类型是IMP,本质上是一个函数指针,下面会介绍

IMP

IMP的定义如下

1
2
3
4
5
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id (*IMP)(id, SEL, ...);
#endif

可以看到IMP指向的方法与objc_msgSend函数类型相同,参数都包含idSEL类型

方法调用流程

我们知道所有分方法调用都被转化成objc_msgSend的调用。当消息发送给一个对象时,objc_msgSend通过对象的isa指针获取到类的结构体,然后在方法分发表里面查找方法的selector。如果没有找到selector,则通过objc_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector。依此,会一直沿着类的继承体系到达NSObject类。一旦定位到selector,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现。下面的图可以表示这样一个框架:

方法调用流程

动态解析

当 Runtime 系统在Cache和方法分发表中(包括超类)找不到要执行的方法时,Runtime会调用resolveInstanceMethod:或resolveClassMethod:来给程序员一次动态添加方法实现的机会。NSObject类中这两个方法都是返回NO的,如果想让我们的类支持消息动态解析,就需要实现这两个函数,用class_addMethod函数完成向特定类添加特定方法实现的操作。下面是一个例子

实现动态解析

当我们用@dynamic修饰属性后,系统就不会为属性创建setter和getter方法,需要我们手动实现。我们这里用动态方法解析实现

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
@dynamic propertyKnowledge;

//首先实现这两个方法,分别是getter和setter
id dynamicPropertyKnowledgeGetterIMP(RTKnowlegdeSub *self, SEL _cmd) {
Ivar var = class_getInstanceVariable([self class], "_propertyKnowledge");
return object_getIvar(self, var);
}

void dynamicPropertyKnowledgeSetterIMP(RTKnowlegdeSub *self, SEL _cmd, id value) {
Ivar var = class_getInstanceVariable([self class], "_propertyKnowledge");
object_setIvar(self, var, value);
}

//在触发动态解析时,将方法添加到类中
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(propertyKnowledge)) {
class_addMethod([self class], sel, (IMP)dynamicPropertyKnowledgeGetterIMP, "@@:");
return YES;
} else if (sel == @selector(setPropertyKnowledge:)) {
class_addMethod([self class], sel, (IMP)dynamicPropertyKnowledgeSetterIMP, "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}

类型编码

class_addMethod的定义如下

1
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);

它的第四个参数是方法的类型字符串,也就是个字符串由 “返回值类型+参数类型” 的类型编码组成。所谓的类型编码就是与Obj-c中类型对应的字符串。编译器会将Object-C中的类型和特定的字符串对应,以帮助运行时系统加快消息分发。如char 对应 “c”、SEL对应 “:” 、”@”对应id、”v”对应void。

如上面的getter函数,返回值类型是id对应@,参数类型是id(接受者)对应”@”, SEL(方法选择器)对应”:”,所以方法的类型字符串就是”@@:”

重定向

当方法动态解析失败(resolveInstanceMethod:resolveClassMethod:返回NO)失败后,会触发方法重定向,Runtime 系统会再给我们一次偷梁换柱的机会,即通过重载forwardingTargetForSelector:方法替换消息的接受者为其他对象。下面我们见length函数的接受者改为一个字符串成员变量。

1
2
3
4
5
6
7
8
9
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if (aSelector == @selector(length)) {
return _propertyString;
}

//当本类不能处理是最好交给父类,不要直接return NO
return [super forwardingTargetForSelector:aSelector];
}

转发

实现转发

当动态解析返回NO,重定向不做处理时,消息转发机制会被触发。在这时forwardInvocation:方法会被执行,我们可以重写这个方法来定义我们的转发逻辑,该方法实现中将消息转发给其它对象。每个对象都从NSObject类中继承了forwardInvocation:方法。然而,NSObject中的方法实现只是简单地调用了doesNotRecognizeSelector:。如果想要我们的类具有消息转发功能需要实现forwardInvocation:,同时在向forwardInvocation:消息发送前,Runtime系统会向对象发送methodSignatureForSelector:消息,并取到返回的方法签名用于生成NSInvocation对象。所以我们在重写forwardInvocation:的同时也要重写methodSignatureForSelector:方法,否则会抛异常。

下面的实现同样是让我们的类可以响应length消息,只不过这次我们使用消息转发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//消息签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
signature = [_propertyString methodSignatureForSelector:aSelector];
}
return signature;
}

//转发
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([_propertyString respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:_propertyString];
} else {
[super forwardInvocation:anInvocation];
}
}

转发和多继承

Objective-c是不支持多继承的,但是消息转发为类添加了多继承的效果,好像我们的类响应了其他类的方法。这些方法好像是我们继承的一样。下面是apple官方文档的一张图。

消息转发

在上图中Warrior和Diplomat没有继承关系,但是Warrior将negotiate消息转发给了Diplomat后,就好似Diplomat是Warrior的超类一样。

消息转发弥补了 Objc 不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。它将问题分解得很细,只针对想要借鉴的方法才转发,而且转发机制是透明的。

尽管转发很像继承,但是NSObject类不会将两者混淆。像respondsToSelector: 和 isKindOfClass:这类方法只会考虑继承体系,不会考虑转发链。比如上图中一个Warrior对象如果被问到是否能响应negotiate消息:

1
if ( [aWarrior respondsToSelector:@selector(negotiate)] )

结果是NO。如果你为了某些意图偏要“弄虚作假”让别人以为Warrior继承到了Diplomat的negotiate方法,你得重新实现 respondsToSelector:isKindOfClass:

完整的转发流程

当一个类不能通过方法调用流程找到对应的方法实现时,就进入了转发流程,下面的图完整的展示了方法的转发流程

message_forward.png

参考

Objective-C Runtime 运行时之三:方法与消息

Objective-C 消息发送与转发机制原理

Objective-C Runtime