RunTime之二:消息传递机制
提醒,阅读本章,需要首先了解上一章节的基础知识。
一、「消息传递」的疑问
面向对象拥有强大的「数据封装」能力,而「消息传递」更是其强大利器之一,那么RunTime是如何实现的呢?
所谓的「消息传递」指的就是「方法的调用」,其中「方法的调用」又可以分为「实例方法」调用 和 「类方法」调用。
其中,「方法的调用」可以分为两个步骤来看,一个根据「方法名」找到「实现」;一个是执行方法的「实现」代码。
例如,定义一个Person类,继承NSObject。
@interface Person : NSObject
- (void)sayHelloWorld;
@end
@implementation ZTwinkleLoadView
- (void)sayHelloWorld {
NSLog(@"HelloWorld!");
}
@end
// 调用实例方法
Person *obj = [[Person alloc] init];
[obj sayHelloWorld]
// 调用类方法
[Person hash]
- 疑问一:调用
[obj sayHelloWorld]
实例方法的时候,Runtime是如何找到sayHelloWorld
方法的呢? - 疑问二:调用
[Person hash]
类方法的时候,Runtime是如何找到hash
方法的呢?
二、「方法」的「查找」过程
「方法调用」的本质:「消息接收者」 找到对应的「方法」SEL
的 「实现」IMP
。
这其实就是一个寻找搜索的过程,而在链表寻找,往往都是不那么快,这也是结构体struct objc_class
需要cache的原因。
2.1、「实例方法」的「查找」过程
- 1、「对象实例」 通过
struct objc_object
里面的isa
指针找到对应的类Class
- 2、首先,会在「类」的结构体
struct objc_class
中的cache
(方法缓存) 的散列表中寻找对应的IMP
(方法实现); - 3、如果缓存中没有找到,会继续在
struct objc_class
中的methodLists
中查找。如果找到,保存到缓存中,并返回; - 4、如果上一步在 「类」 中没有找到,会通过
struct objc_class
中的superClass
指针找到其父类,然后在父类中method_list
中查找;一直往下,直到基类。 - 5、上面步骤中,一旦找到「方法,SEL」的「实现,IMP」,就会立即缓存,并且执行
objc_msgSend
函数,执行「方法」的「实现」。 - 6、假设上面步骤中,都没有找到「方法」的「实现」,消息将会被转发,进入「消息异常捕获逻辑」。

2.2、「类方法」的「查找」过程
其查找的过程 和 「实例方法」的「查找」过程
的过程类似,不一样的是寻找的是沿着「元类」的路径。
其大致的过程如下:
- 1、通过「类对象」
struct objc_class
的isa
指针 找到所属的 「Meta Class(元类)」; - 2、剩下的「类方法」查找过程 和 「类方法」的执行,同实例方法一样。如上所示。
- 3、如果没有查找到,也会进入到「消息异常捕获逻辑」。
2.3、解答疑问:
-
疑问一:调用
[obj sayHelloWorld]
实例方法的时候,Runtime是如何找到sayHelloWorld
方法的呢?
调用[obj sayHelloWorld]
实例方法的时候,obj
首先查找缓存cache,找到执行;否则在methodLists
链表找到sayHelloWorld
方法的实现,然后保存到cache中,然后返回。
如果是调用[obj copy]
会在父类NSObject
中找到该方法。
如果是调用[obj xxx]
未被定义的方法,会进入到异常捕获流程。 -
疑问二:调用
[Person hash]
类方法的时候,Runtime是如何找到hash
方法的呢?
调用[Person hash]
类方法的时候,会先找到Person
的「元类」,然后在元类的缓存中找,没找到,在方法列表中找;如果还是没有找到,会通过superClass
指针找到父类NSObject
,然后重复查找过程,知道找到返回,并且执行。
2.4、若干问题
-
关于「MetaClass,元类」
每一个类都有对应的元类,元类的结构体定义同objc_class
一样。「元类」对于开发中来说一般是不可见,也不建议去操作它。 -
一个问题,「元类」的定义同
struct objc_class
一样,那么「元类」也有isa指针,它又指向哪里呢?
为了不让这种结构无限延伸下去, Objective-C 的设计者让所有的「元类」的
isa指向「基类」(比如 NSObject )的「元类」。而「基类」的「元类」的
isa` 指向自己。这样就形成了一个的闭环。 -
一个问题,「基类」也有
super_class
指针,它又指向哪里呢?
我们知道,子类通过super_class
指向父类,依次往下,直到「基类」的super_class
指向 nil 。这样就形成了一个的闭环。
OC中类的继承链,也是通过super_class
实现的。由于objc_class
只有一个superClass
指针,因此它也只能支持单继承。
可以推断,如果多继承,那么这个链路就会显得太过于复杂;另一方面,多继承在实际开发中并不常见,而且推荐少用或者不用,或者可以通过其他方式(例如接口)实现设计意图,因此设计者舍弃。
2.5、一张图说明「实例方法」和「类方法」的查找过程

注意图中,实线表示
superClass
的查找过程,虚线表示isa
指针的查找过程。
三、「方法」的「执行」过程,即objc_msgSend
所有 Objective-C 方法调用在编译时都会转化为对 C 函数 objc_msgSend
的调用。
objc_msgSend
函数的定义:id objc_msgSend(id receiver, SEL op, ...)
,前面两个参数固定,第一个参数是接收者id
,第二个参数方法名SEL
,后面的...
可变参数列表,传递函数的参数。
objc_msgSend
函数的定义和 IMP = id + SEL + ...(即参数列表)
的定义是一模一样的。因此,查找到方法的具体「实现」,找到是IMP
执行,具体执行代码的是objc_msgSend
函数。
至于objc_msgSend
函数的内部代码实现,有兴趣的同学可以深入学习下。深入解构objc_msgSend函数的实现
四、方法找不到的异常处理
现实开发中,经常会遇到一种Crash,就是“unrecognized selector sent to instance
,意思是某个实例或者类找不到某个方法名”,这背后的处理机制如何呢?有没有避免Crash的统一处理机制呢?带着问题,继续往下看。
从上面关于「实例方法」和「类方法」的查找过程中,它们最后都有个异常处理逻辑,就是当方法找不到的时候进入「消息异常捕获逻辑」或者「消息的转发机制」。
4.1、「消息的转发机制」的原理图
这个「消息异常捕获逻辑」内部机制又是怎么样的呢?
OC设计一套异常的处理机制,即「消息的转发机制」,当某个方法名找不到对应的「实现」的时候,给你个机会处理这种异常。这种能力就为我们提供 “方法找不到Crash” 的统一拦截处理方案。

4.2、「消息的转发机制」可以分为三个步骤:
- 1、「消息动态解析」
对应有两个方法捕获,分别是,「类方法」+ (BOOL)resolveClassMethod:(SEL)sel
、「实例方法」+ (BOOL)resolveInstanceMethod:(SEL)sel
。
通常的做法是「动态添加一个方法」,并返回 YES 告诉程序已经成功处理消息。如果这两个方法返回 NO ,这个流程会继续往下走。「动态添加方法」一般调用BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);
函数来实现。
// 重写 resolveInstanceMethod: 添加对象方法实现
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(xxx_unrecognized_func)) {
class_addMethod([self class], sel, (IMP) xxx_unrecognized_func, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void xxx_unrecognized_func(id obj, SEL _cmd) {
...
}
上面的例子中,class_addMethod
的特殊参数 v@:
称为 Type Encodings
,其具体说请跳转 传送门
- 2、「消息接收者的重定向」
在第一步的时候,resolveInstanceMethod:
没有添加其他函数实现,运行时就会进行下一步,进行「消息接收者的重定向」。对应的方法是:- (id)forwardingTargetForSelector:(SEL)aSelector
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(xxx_unrecognized_func)) {
return [[xxx_Class alloc] init];
}
return [super forwardingTargetForSelector:aSelector];
}
-
3、「消息重定向」
如果错过了「消息动态解析」、「消息接收者重定向」两次机会后,NSObject提供最后一次机会「消息重定向」。其大致的过程如下:
a、提供「函数签名」即NSMethodSignature
对象,「函数签名」 = 函数的参数 + 返回值类型。Runtime会通过以下方法获取「函数签名」
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector
b、如果
methodSignatureForSelector:
返回合法的NSMethodSignature
对象;Runtime会创建 「调用者」即NSInvocation
对象,然后并通过forwardInvocation:
消息通知当前对象,方法里面指定某个对象来处理某个方法。
对应方法:- (void)forwardInvocation:(NSInvocation *)anInvocation
c、如果
methodSignatureForSelector:
返回nil,则Runtime 系统会发出doesNotRecognizeSelector:
消息,程序也就崩溃了。
对应方法:- (void)doesNotRecognizeSelector:(SEL)aSelector;
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
return nil;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if ([NSStringFromSelector(aSelector) isEqualToString:@"xxx_unrecognized_func"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;
xxx_Class *obj = [[xxx_Class alloc] init];
if([obj respondsToSelector:sel]) {
[anInvocation invokeWithTarget:obj];
} else {
[self doesNotRecognizeSelector:sel];
}
}
4.3、注意
异常解析的方法都被定义在NSObject根类中,由于OC所有的类都是继承来自NSObject,因此所有的类或者类的实例都可以处理异常消息。
4.4、利用NSInvocation
实现动态方法调用
即随意指定某个对象,执行某个方法。如下
NSMethodSignature *sign = [UIView.class instanceMethodSignatureForSelector:@selector(setBackgroundColor:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sign];
[invocation invokeWithTarget:viewObj_1];
[invocation invokeWithTarget:viewObj_2];
[invocation invokeWithTarget:viewObj_3];
其他
RunTime原理之一:如何让Objective-C支持面向对象的能力呢?
Runtime:消息发送机制、动态方法解析、消息转发机制
iOS 开发:『Runtime』详解(一)基础知识