先总结,后解释
Objective-C当向一个对象发送消息时,寻找消息的顺序
1.寻找类自身的方法实现
先会调用objc_msgSend
方法,首先在Class中的缓存和方法列表中查找IMP。
2.寻找父类的方法实现
如果该类中没有找到,则向父类的Class查找。如果一直查找到根类仍旧没有找到,则执行消息转发。
3. 动态添加模式
调用resolveInstanceMethod:(实例方法)
或resolveClassMethod:(类方法)
方法。允许用户在此时为该Class动态添加实现方法。如过实现了,调用并返回YES,重新开始objc_msgSend流程。如果仍没有实现,继续下面的动作。
4.快速向前转发模式
调用forwardingTargetForSelector:
方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,并返回非nil对象。否则返回nil,继续下面的动作。
5.正常向前转发模式
调用methodSignatureForSelector:
方法,尝试获得一个方法签名。
如果获取不到,则直接调用doesNotRecognizeSelector
抛出异常。
如果能获取,则返回非nil并调用forwardInvocation:
方法,将获取到的方法签名包装成Invocation传入。在forwardInvocation:
内指定消息接收者来处理消息(如果不指定也不会报错了)。
6.异常处理
调用doesNotRecognizeSelector
抛出异常。重写doesNotRecognizeSelector也可自定义异常的抛出。
通过一个示例来了解消息转发机制
- 实现
Animal
类
只在.h
中声明了方法,不在.m
中实现该方法。
#import <Foundation/Foundation.h>
@interface Animal : NSObject
- (void)eatTogetherWith:(Animal *)animal;
@end
#import "Animal.h"
@implementation Animal
@end
- 在
ViewController
中实现Animal并调用该方法。
#import "ViewController.h"
#import "Animal.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Animal *one = [[Animal alloc] init];
Animal *two = [[Animal alloc] init];
[one eatTogetherWith:two];
}
@end
- 此时会崩溃,崩溃信息为
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Animal eatTogetherWith:]: unrecognized selector sent to instance 0x600001fd3280'
这个过程,系统做了什么呢?
向对象发送某个消息的时候,编译器会调用底层的 obj_msgSend()
C函数,从缓存和方法列表中,寻找对象的函数指针(IMP),如果找到,则执行。否则继续根据一定的策略或者方式继续找,最终如果没有找到,则让程序 Crash, 报一个异常。
objc_msgSend的作用是向一个实例类发送一个带有简单返回值的message,是一个参数个数不定的函数。当遇到一个方法调用,编译器会生成一个objc_msgSend的调用,有:objc_msgSend_stret、objc_msgSendSuper或者是objc_msgSendSuper_stret。发送给父类的message会使用objc_msgSendSuper,其他的消息会使用objc_msgSend。如果方法的返回值是一个结构体(structures),那么就会使用objc_msgSendSuper_stret或者objc_msgSend_stret。 第一个参数是:指向接收该消息的类的实例的指针;第二个参数是:要处理的消息的selector; 其他的就是要传入的参数。这样消息派发系统就在接收者所属类中查找器方法列表,如果找到和选择器名称相符的方法就跳转其实现代码,如果找不到,就再其父类找,等找到合适的方法在跳转到实现代码。这里跳转到实现代码这一操作利用了尾递归优化。 如果该消息无法被该类或者其父类解读,就会开始进行消息转发。
1. 从缓存和方法列表中,寻找对象的函数指针(IMP)
当编译器看到这条消息的时候,就会转换为一条标准C函数:id objc_msgSend(id self, SEL _cmd, …)
,此时会将[one eatTogetherWith:two]
变为:
objc_msgSend(one,@selector(eatTogetherWith:),two)
。
- 查看缓存中是否有匹配的函数指针(IMP),如果有则执行,否则继续。
- 查看方法列表中是否有匹配的函数指针(IMP), 如果有则执行,否则继续。
- 查看父类中是否有匹配的函数指针(IMP), 如果有则执行,否则继续。
最终如果没有找到,则让程序 Crash, 报一个异常:unrecognized selector sent to instance
。
2. 系统第一次挽救Crash:动态添加模式
系统会查询该类中是否有动态方法解析。如果有则执行,否则继续。
需要重写对应的方法实现动态方法解析
// 在 Animal.m中
/// 如果是实例方法,就重写该方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSString *method = NSStringFromSelector(sel);
if ([method isEqualToString:@"eatTogetherWith:"]) {
class_addMethod(self, sel, (IMP)dynamicMethodIMP, "v@:@");
}
return [super resolveInstanceMethod:sel];
}
/// 如果是类方法,就重写该方法
+ (BOOL)resolveClassMethod:(SEL)sel {
NSString *method = NSStringFromSelector(sel);
if ([method isEqualToString:@"eatTogetherWith:"]) {
class_addMethod(self, sel, (IMP)dynamicMethodIMP, "@@:");
}
return [super resolveClassMethod:sel];
}
/**
要动态绑定的方法
@param self 要绑定方法的对象
@param _cmd 方法信息
@param value 方法参数
*/
void dynamicMethodIMP(id self,SEL _cmd,id value) {
NSString *sel = NSStringFromSelector(_cmd);
NSLog(@"self = %@ _cmd = %@ value = %@", self, sel, value);
}
// 打印结果: self = <Animal: 0x6000019a2d40> _cmd = eatTogetherWith: value = <Animal: 0x6000019a2d30>
当消息传递无法处理的时候,首先会看一下所属类,是否动态添加了方法,以处理当前未知的选择子。这个过程叫做“动态方法解析”(dynamic method resolution)
说明:
- 该方法是实例方法时调用
resolveInstanceMethod :
,该方法是类方法时调用resolveClassMethod :
。 -
v@:@
的含义(依次)- v: 表示返回类型是void
- @:表示id (self receiver)
- 冒号:表示SEL
- @:方法的具体参数
- 动态方法解析确切的说还不属于消息转发的过程,是在消息转发之前对实例方法或类方法进行补救。
3. 第二次挽救Crash:快速向前转发模式
快速消息转发也叫备援接收者/消息重定向,如果有指定消息接收对象则将消息转由接收对象响应,否则继续。
新增一个Dog类,并实现一个同名的方法
#import <Foundation/Foundation.h>
#import "Animal.h"
@interface Dog : NSObject
- (void)eatTogetherWith:(Animal *)animal;
@end
#import "Dog.h"
@implementation Dog
- (void)eatTogetherWith:(Animal *)animal {
NSLog(@"Dog类实现了eatTogetherWith方法");
}
@end
在Animal里面添加以下代码,并注释掉动态方法解析的代码。
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSString *method = NSStringFromSelector(aSelector);
if ([method isEqualToString:@"eatTogetherWith:"]) {
Dog *dog = [[Dog alloc] init];
return dog;
}
return nil;
}
// 输出: Dog类实现了eatTogetherWith方法
此时Animal类的eatTogetherWith:
方法就通过快速消息转发模式转给了Dog类处理了。
4. 第三次挽救崩溃:正常向前转发模式
当动态添加模式和快速向前转发模式都没处理消息的话,会执行** 正常向前转发模式**。
如果有指定转发对象则转发给该对象响应,否则抛出异常。
先实现方法签名(注释掉上面两次的挽救代码)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSString *methodName = NSStringFromSelector(aSelector);
if ([methodName isEqualToString:@"eatTogetherWith:"]) {
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
return [super methodSignatureForSelector:aSelector];
}
指定消息接收者
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = [anInvocation selector];
Dog *dog = [[Dog alloc] init];
if ([dog respondsToSelector:sel]) {
[anInvocation invokeWithTarget:dog];
return;
}
[super forwardInvocation:anInvocation];
}
调用methodSignatureForSelector:
方法,尝试获得一个方法签名。
如果获取不到签名,则直接调用doesNotRecognizeSelector:
抛出异常。
如果能获取到,则返回非nil并调用forwardInvocation:
方法,将获取到的方法签名包装成Invocation传入。在forwardInvocation:
内指定消息接收者来处理消息。
此时不指定消息接收者也不会报错了。
5. 抛出异常
如果三次都没拯救,就调doesNotRecognizeSelector
, 默认的实现是抛出异常。如果想更改异常内容可以重写该方法。
- (void)doesNotRecognizeSelector:(SEL)aSelector {
NSString *method = NSStringFromSelector(aSelector);
if ([method isEqualToString:@"eatTogetherWith:"]) {
NSLog(@"Animal 无法执行 eatTogetherWith:方法,特抛出异常告知。");
}
}
延伸
1. 什么是消息?
崩溃的原因是执行了[one eatTogetherWith:two]
方法,Animal
类或者其父类中没有找到[Animal eatTogetherWith:]
这个方法。
- one叫做消息接收者
- eatTogetherWith叫选择器
- two是参数
- 消息 = 选择器+参数
2. SEL和IMP是什么?
[one eatTogetherWith:two];
可以换成
[one performSelector:@selector(eatTogetherWith:) withObject:two];
两者作用相同,都是向one这个实力发送一条eatTogetherWith:
的消息,参数都是two。
这里的@selector(eatTogetherWith:)
是消息的选择器或者选择子。
- SEL:编译过程中,会根据方法的名字生成类型是 SEL的唯一 ID。通过方法名字(NSString)可以找到ID。
- IMP: 是一个函数的具体实现,是一个函数指针。这个函数指针指向的函数。至少有两个参数:
- 第一个参数:id self, 接收消息的对象(receiver)
- 第二个参数:SEL _cmd, 方法名
3. 静态绑定/动态绑定
- 静态绑定: 在编译期间就能决定运行所调用的函数。
- 动态绑定: 在运行期才能确定调用函数。
在OC中, 对象发送消息,就会使用动态绑定机制来决定需要调用的方法。当对象收到消息后,究竟调用哪个方法完全决定于运行期,甚至也可以直接在运行时改变方法,这些特性都使OC成为了一门动态语言。
4. 关于Swift
Objective-C有运行时机制,具备动态性,但是Swift没有。Swift是继承了Objective-C有的runtime机制才有了动态性。尝试用Swift来处理消息转发。
- 动态方法解析
// 动态方法解析
override class func resolveInstanceMethod(_ sel: Selector!) -> Bool {
guard let method = class_getInstanceMethod(self, #selector(runIMP)) else {
return super.resolveInstanceMethod(sel)
}
return class_addMethod(self, Selector(("run")), method_getImplementation(method), method_getTypeEncoding(method))
}
@objc func runIMP() {
print("runIMP")
}
- 快速向前转发模式
// 快速消息转发
override func forwardingTarget(for aSelector: Selector!) -> Any? {
return Dog()
}
class Dog : NSObject {
@objc func eat() {
print("eat")
}
}
- 正常向前转发模式
在Swift中去除了methodSignatureForSelector:
和forwardInvocation:
这两个方法,在Swift中只有动态方法解析和快速消息转发去实现了。 - 错误处理
override class func doesNotRecognizeSelector(_ aSelector: Selector!) {
}