深入理解Objective-C:方法调用

我们知道,Objective-C 是一门面向对象的动态语言,是C语言的超集。而 OC 之所以具有这些特性,本质上在于 Runtime 库。Runtime 由C语言和汇编语言编写,是 OC 语言的“基石”,它定义了 OC 语言的基础数据结构;类与对象的相关操作函数;方法调用、消息机制;Protocol 和 Category 等。理解 Objective-C 的 Runtime 可以帮我们更好的了解这个语言,适当的时候还能对语言进行扩展,从系统层面解决项目中的一些设计或技术问题。本文从 Runtime 层研究 OC 语言的方法调用、消息机制。

先看下与 runtime 密切相关的几个数据类型: Class, SEL, IMP, Method,他们都定义在 objc/objc.h 文件中。

Class, Method, SEL, IMP

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
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

/// A pointer to an instance of a class.
typedef struct objc_object *id;

typedef struct objc_object {
Class isa;
} *id;
/*可以看到,iOS 中的 id 类型实际上就是指向 objc_object 结构体的指针,
objc_object 持有一个 Class 类型的 isa 变量。

NSObject 的定义

@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}

NSObject 同样也持有一个 Class 类型的 isa 变量,但是额外遵循 NSObject 协议。
因此 id 可以泛指所有 NSObject 以及继承自 NSObject 的对象,
但是 NSObject * 指针却不能指向 id 类型。
*/


typedef struct objc_selector *SEL;

#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id (*IMP)(id, SEL, ...);
#endif
1. Class

Class 被定义为一个指向 objc_class 的结构体指针。objc_class 在 objc/objc_class.h 中定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

我们发现,Class 本身也有一个 isa 指针,指向的是它的 MetaClass。下面这张图说明了 OC 对象体系 isa 以及 super_class 指针的指向:

2. SEL

SEL 的定义如下:

1
typedef struct objc_selector    *SEL;

在源码中没有直接找到 objc_selector 的定义,从一些书籍上与 Blog 上看到可以将 SEL 理解为一个 char* 指针:

1
2
3
4
struct objc_selector {
char *name; OBJC2_UNAVAILABLE;
char *types; OBJC2_UNAVAILABLE;
};

3. IMP

IMP 定义如下:

1
typedef id (*IMP)(id, SEL, ...);

IMP 可以理解为函数指针,指向了函数的实现代码。这个被指向的函数的参数包含一个接收消息的 id 对象、 SEL、以及不定个数的参数,并返回一个 id 类型的对象,我们可以像在C语言里面一样使用这个函数指针。

SEL 与 IMP 的关系非常类似于 HashTable 中 key 与 value 的关系。OC 中不支持函数重载的原因就是因为一个类的方法列表中不能存在两个相同的 SEL 。但是不同的类中可以拥有相同的 SEL,不同类的实例对象执行相同的 SEL 时,会在各自的方法列表中去根据 SEL 去寻找自己对应的IMP。这使得OC可以支持函数重写。

4. Method

Method 的定义如下:

1
2
3
4
5
6
7
8
/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method;

struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}

Method = SEL + IMP + method_types,相当于在SEL和IMP之间建立了一个映射。

方法调用

我们都知道,在 Objective-C 里调用一个方法是这样的:

1
[obj method];

表示我们要调用对象 obj 的方法 method。Runtime 层会将这个调用翻译成objc_msgSend(self, _cmd),其函数原型为:

1
id objc_msgSend(id self, SEL op, ...);

也就是说,对象的方法调用,在 runtime 层表达为向该对象发送消息,消息机制也是OC动态特性的本质,类似于 Ruby。而 objc_msgSend具体又是如何分发的呢?通过阅读它的源码文件objc-msg-arm64.s ,可以看到,objc_msgSend 的消息分发有以下几个步骤::

苹果开源的项目中共有 6 种对不同平台的汇编实现,本节选取其在 arm64 下的源码文件

  1. 判断 receiver(objc_msgSend的第一个参数self) 是否为 nil。ObjC 的特性是允许对一个 nil 对象执行任何一个方法不会 Crash,因为会被忽略掉
  2. 从 cache 中查找 SEL 对应的 IMP,若找到,则执行,否则
  3. 从对象的方法链表中根据 SEL 去查找 IMP,找到后加入到缓存。查找过程如下:
    • 如果支持GC,忽略掉非GC环境的方法(retain等)
    • 从对象的 Class 的 method_list 寻找 selector,如果找到,填充到缓存中,并返回 selector,否则
    • 从对象的超类的 method_list 寻找,并依次往上寻找,直到找到 selector,填充到缓存中,并返回 selector,否则
    • 调用 _class_resolveMethod,动态方法决议,若找到 selector 对应的方法实现 IMP,不缓存,方法返回,否则
    • 转发这个 selector,否则
  4. 报错,抛出异常

以上便是方法调用的整个过程,下面重点说下动态方法决议以及消息转发

动态方法决议

当无法从对象(类对象或实例对象)以及对象父类的 cache 和 method_list 中找到 selector 对应的 IMP 时,就会进入动态方法决议阶段,在这个阶段,如果能够提供 seltctor 的方法实现,并反回YES,则成功处理该消息,本次方法调用结束,不会再进行后续的消息转发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Dynamically provides an implementation for a given selector for an instance method.
+ (BOOL)resolveInstanceMethod:(SEL)sel;

or

//Dynamically provides an implementation for a given selector for a class method.
+ (BOOL)resolveClassMethod:(SEL)sel;


return value:
YES if the method was found and added to the receiver, otherwise NO.

返回 YES 表示不进行后续的消息转发,返回 NO 则表示要进行后续的消息转发。

如果在该函数内为指定的 selector 提供实现,无论返回 YES 还是 NO,编译运行都是正确的;
但如果在该函数内并不真正为 selector 提供实现,无论返回 YES 还是 NO,运行都会 crash

resolveInstanceMethod: 以及 resolveClassMethod:允许你动态提供给定 selector 的 IMP 实现。

简单的说,Objective-C 方法(method)就是至少带有两个参数:self_cmd 的 C 函数。使用 class_addMethod 函数可以将 C 函数添加到某个类中作为该类的一个方法。

比如有下面这样一个C函数:

1
2
3
4
void dynamicMethodIMP(id self, SEL _cmd, NSString *str)
{
// implementation ....
}

可以使用 resolveInstanceMethod: 动态的将这个函数添加到类中作为该类的一个方法(假设方法名为:resolveThisMethodDynamically):

1
2
3
4
5
6
7
8
9
+ (BOOL) resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically))
{
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:@");
return YES;
}
return [super resolveInstanceMethod:aSel];
}

其中 class_addMethod 的函数原型如下:

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

刚开始让我不太明白的是最后一个参数 types ,查阅资料后得知其是一个定义该函数返回值类型和参数类型的字符串。本例中 types 为 "v@:@",按照顺序表示如下:

1
2
3
4
v    :   返回值为 void
@ : 参数id(self)
: : SEL(_cmd)对象
@ : 参数id(str)

也就是说,types 描述了一个函数的返回值类型以及参数类型。: 表示 SEL 类型,@ 表示 OC 对象类型。其他类型见下表:

字符 表示的意义
c char 类型
i int 类型
s short 类型
l long 类型
q long long 类型
C unsigned char 类型
I unsigned int 类型
S unsigned short 类型
L unsigned long 类型
Q unsigned long long 类型
f float 类型
d double 类型
B C++ bool or C99 _Bool 类型
v void 类型
* character string (char *) 类型



特别注意:
resolveInstanceMethod: and resolveClassMethod: is called before the Objective-C forwarding mechanism is invoked. If respondsToSelector: or instancesRespondToSelector: is invoked, the dynamic method resolver is given the opportunity to provide an IMP for the given selector first.

消息转发

如果动态方法决议不能处理当前消息(返回 NO),那么就会走到消息转发阶段。

1
2
3
4
5
6
7
8
9
//Returns the object to which unrecognized messages should first be directed.
- (id)forwardingTargetForSelector:(SEL)aSelector;

Parameters:
aSelector
A Selector for a method that the receiver does not implement.

Return Value:
The object to which unrecognized messages should first be directed.

如果你在一个类实现了该方法,并返回一个非空对象(非 self),那么返回的对象作为新的消息接收对象,这条消息会重新发送给该对象。很显然,如果你在上述方法中返回 self ,代码会进入死循环。

如果你在一个类(非基类)中实现了该方法,并且对于给定的 selector 没有给出返回对象,那么你应该返回超类的方法调用结果:return [super forwardingTargetForSelector:aSelector]

这个方法给对象一次重定向(redirect)未知消息的机会,这先于代价更高的 forwardInvocation: 。当你想简单的把消息重定向到另一个对象时,使用这个方法会非常有用,而且效率会比通常的消息转发 forwardInvocation 快一个数量级。

如果上面的一波操作还是没能处理该消息,就会进入下面的步骤。

1
2
3
4
5
//Overridden by subclasses to forward messages to other objects.
-(void)forwardInvocation:(NSInvocation *)invocation

Parameters:
anInvocation : The invocation to forward.

如果一个对象(receiver)收到一个自己不能处理的消息(找不到对应的方法),runtime 会给这个对象一次机会将该消息委托给另外一个/多个接收者(another receivers)去处理。首先 runtime 会创建一个 NSInvocation 对象(它包含这个消息的完整信息),并向 receiver 发送 forwardInvocation: 消息,参数为刚才创建的 NSInvocation 对象。receiver 对象的 forwardInvocation: 方法就可以将这个消息转发给 another receivers (如果这个对象还是不能响应该消息,它仍有机会去转发)。

forwardInvocation: 让一个对象(receiver)和另外的对象(another receivers)建立起联系,另外的对象会代其行事。所以,在某种意义上,进行转发的对象(receiver)便“继承”了将消息转发到的对象(another receivers)的某些特性。

注意:

刚才提到,当对象(receiver)不能处理某个消息(aSelector)时,runtime 在发送 forwardInvocation: 消息给这个对象(receiver)之前,会创建一个 NSInvocation 对象用于转发传参。但是创建 NSInvocation 对象需要的信息要从 methodSignatureForSelector: 中获取。所以除了 forwardInvocation: 之外,你还必须额外重写 methodSignatureForSelector: 方法来获取该 SEL 的方法签名。

实现 forwardInvocation: 方法需要做两件事:

  • 找到一个能够响应封装在 anInvocation 中的消息的对象
  • 使用 anInvocation 将消息发送给上一步的对象,anInvocation 会保留消息处理结果,runtime 获取这个结果并发送给原始的消息接收者

举个简单的例子:
一个对象仅仅将消息转发给另外一个对象(假设为 friend 实例对象),forwardInvocation: 方法的实现如下:

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

- (void)forwardInvocation:(NSInvocation *)invocation
{
SEL aSelector = [invocation selector];

if ([friend respondsToSelector:aSelector])
[invocation invokeWithTarget:friend];
else
[super forwardInvocation:invocation];
}

被转发的消息必须有固定个数的参数,可变个数的参数(比如 printf())是不支持的。

被转发的消息的返回值,会被返回得到原始消息接收者,也就是 forwardInvocation: 消息的原始发送者,所有类型的返回值都支持,比如id类型,结构体,double等。

NSObject 类对 forwardInvocation: 方法的实现是:简单的调用 doesNotRecognizeSelector: 方法,它不转发任何消息。因此,如果你没有实现 forwardInvocation:,并且给对象发送了一个不能识别的消息,就会发生异常。

动态方法决议和消息转发流程可总结为下面这张图:

self 与 super 的含义

在进行方法调用时,我们通常这样写(假设当前类和其超类都有 method 实例方法):

1
2
3
4
5
[self method]

or

[super method]

按照我最初模糊的理解,self 代表当前接收这个消息的对象,super 代表超类。但是仔细想下,这样理解是有问题的,如果 super 代表超类,那 super method 就表示调用了超类的类方法 method,但是这个方法明明是实例方法呀。而且 super 更不可能代表超类的实例对象,因为你都没创建。带着疑惑,我决定仔细研究下。

根据官方文档的解释:

self 代表当前接收这个消息的对象

super 发送一个消息,表示去当前对象的超类中调用某个方法。

所以说,self 代表的是一个真实的对象;super 仅表示“去当前对象的超类中调用某个方法”,它是编译器的一个符号,不是对象。

理解了他们的含义,那么在向 self 和 super 发送消息时,在 runtime 层是怎样区别的?

向 self 发送消息,runtime 会调用下面这个函数:

1
id objc_msgSend(id self, SEL op, ...);

向 super 发送消息时,runtime 会调用下面的函数:

1
id objc_msgSendSuper(struct objc_super *super, SEL op, ...);

结构体 objc_super 的定义如下:

1
2
3
4
struct objc_super {
id receiver;
Class superClass;
};

所以,向 super 发送消息时,消息的接收者,依然是当前对象 self。只不过是从超类的 method_list 中查找方法实现。

到这里,应该就理解下面这个问题的答案了:

1
2
3
4
5
6
7
8
9
10
@implementation Dog : Animal
- (id)init {
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end

会分别打印出来什么?

肯定都是打印 Dog 。