消息转发概述
Objective-C是一门动态语言,怎么理解动态这一词呢?简单的说就是编译器在编译期可以只知道一个方法的名字,而不需要知道这个方法的实现,只有在运行期间调用该方法的时候,才根据方法名去找到对应方法的实现,这个过程相当于动态绑定一个方法的实现,这就是“动态”。
与“动态”相对的是“静态”,C语言就是一门静态语言,在编译期不仅知道运行时所要调用的函数的名字,而且直接生成了调用函数的指令,将函数地址硬编码在这些指令中。这就是为什么OC的方法没有实现只有声明编译不报错,而C的方法却报错的原因。
正因为是动态绑定,所以在编译期没有报错的程序,在运行时由于根据方法名找不到对应的方法实现,会导致程序的Crash,在Crash之前程序会依次调用几个其他的方法,这就引出了消息转发。
消息转发过程
消息转发是Objective-C语言的特点,当一个对象在运行时接收到无法解读的消息时,就会触发“消息转发”。
在编译期向类发送无法解读的消息是不会报错的,因为在运行期可以继续向类添加方法,所以在编译期,编译器无法知道到底有没有某个方法的实现。
可能听起来有点乱,又是向对象发送无法解读的消息,又是向类发送无法解读的消息。前者可理解为只在运行期有的行为(因为有对象,那肯定是调用了生成对象方法的实现才可能存在,而只有运行期才去调用方法的实现),后者看下面这句代码就知道了[self performSelector:@selector(humenName)];
方法“humenName”我并没有声明也没有实现,但是这句代码不会报错(会报警告),编译也可以通过,这就是所谓的编译期向类发送无法解读的消息。
消息转发的过程是有一定的规则和步骤的。下面我们看看详细的流程。
1.先看一个runtime库的方法 class_addMethod
class_addMethod的用处是在程序运行时,给一个类添加方法实现的API,其完整API如下:
/**
* 根据指定的名字和方法实现给一个类添加方法.
*
* @param cls 被添加方法的类.
* @param name 指定要添加的方法名称的选择器。
* @param imp 新的方法实现,这个方法必须至少带有两个参数:self和_cmd
* @param types 上面那个新方法的参数的类型编码.
*
* @return 当方法添加成功返回YES , 否则返回NO
* (例如,在类中已经有一个该名字的方法实现,会返回NO)
*
* @note class_addMethod 可能会覆盖超类实现, 如果超类也实现了该方法的话,
* 但是不会替换在本类中已经存在的方法实现,
* 如果改变本类存在的方法实现,请使用method_setImplementation.
*/
OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types)
这个API在消息转发中用到。消息转发分为三个阶段,即“动态方法解析”、“快速消息转发”和“完整消息转发机制”。
2. 动态方法解析
这里结合一个实例来说明,可能会更加容易懂些,新建一个项目,创建一个类HumenModel(继承自NSObject),然后在ViewController中添加如下代码
HumenModel *model = [HumenModel new];
[model performSelector:@selector(humenName)];
因为在HumenModel类中并没有方法humenName的声明和实现,所以,对象model会接受到一个无法解析的消息,此时就会进入消息转发的第一阶段,征询接收者所属的类,看是否能动态添加方法,以处理这个未知的“选择器”,此时就调用该类的类方法
+(BOOL)resolveInstanceMethod:(SEL)sel
当然,如果是类接收到一个无法解析的消息,消息转发第一阶段调用的是类的另一个方法:
+(BOOL)resolveClassMethod:(SEL)sel
方法的参数 sel 就是那个未知“选择器”,返回值是BOOL类型,表示是否新增一个方法来处理未知“选择器”。 现在我们就可以通过class_addMethod方法给类添加一个方法来处理未知“选择器”,代码如下:
+(BOOL)resolveInstanceMethod:(SEL)sel{
// 获取选择器的方法名字
NSString *selString = NSStringFromSelector(sel);
if ([selString isEqualToString:@"humenName"]) {
// 给接收者self 添加一个方法sayHello,选择器sel指向方法的实现,方法的类型编码是v@:
class_addMethod(self, sel, (IMP)sayHello, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
// 一个c函数
void sayHello(id self, SEL _cmd){
NSLog(@"hello");
}
解释下class_addMethod方法的第四个参数“v@:”
这是sayHello方法的OC类型编码,‘v’表示返回值为void类型,‘@’表示第一个参数是对象,‘:’表示第二个参数是SEL类型的值,其中'@'和':'是固定的,因为每个方法都会有这两个参数。对于其他情况可参照类型编码这篇博客https://blog.csdn.net/ssirreplaceable/article/details/53376915
注意,所添加的方法必须是纯C函数实现的,因为OC的方法名规则和C函数名规则差别是很大的。另外,在运行时,方法resolveInstanceMethod中的代码会被动态插在类里面.
编译运行,打印结果如下:
2019-02-20 17:31:53.411603+0800 MessageTrans[400:8535618] hello
跟预期的结果一样,这样就完成了动态方法解析,无法解析的消息在这一步得到了处理。如果在这阶段没有针对未知“选择器”的做出处理,那么就会进入消息转发的第二阶段。
3.快速消息转发机制
在这一阶段,接收者将要甩锅,看有没有别的接收者可以处理这个无法解析的消息(记得将第一阶段的代码注释掉)。这个一过程会在接收者所在类的下面这个方法中完成
-(id)forwardingTargetForSelector:(SEL)aSelector
参数aSelector是未知选择器,返回值是id类型的值,所以这一阶段只是针对对象来处理,不考虑类方法。
新建一个AnimalModel类,在其实现文件中实现方法humenName,如下:
-(void)humenName{
NSLog(@"%s",__FILE__);
}
然后在HumenModel.m中实现forwardingTargetForSelector:方法,如下:
-(id)forwardingTargetForSelector:(SEL)aSelector{
NSString *aSelectorString = NSStringFromSelector(aSelector);
if ([aSelectorString isEqualToString:@"humenName"]) {
return [AnimalModel new];
}
return [super forwardingTargetForSelector:aSelector];
}
这样消息交由AnimalModel类处理,运行一下,打印结果如下:
2019-02-20 17:36:49.000246+0800 MessageTrans[2513:8550047] /Users/xiangzuhua/Desktop/MessageTrans/MessageTrans/AnimalModel.m
结果正确,我们在第二阶段成功的将消息转发给其他接收者来处理。
对于AnimalModel类,不需要在其头文件中声明humenName方法,没有影响。
如果没有其他接收者,那就会进入“完整消息转发机制”阶段。
4.完整消息转发机制
在这个阶段要处理未知消息,代价就会大些,其实也是类似于快速消息转发阶段,目的都是指定一个接受消息的对象,只不过这里必须覆盖两个方法,即methodSignatureForSelector:和forwardInvocation:。
methodSignatureForSelector:的作用在于为另一个类实现的消息创建一个有效的方法签名,必须实现,并且返回不为空的methodSignature,否则会crash。
forwardInvocation:的作用是绑定消息接收者。
看代码实现:
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if ([super methodSignatureForSelector:aSelector] == nil) {
// 手动创建
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
return signature;
}
return [super methodSignatureForSelector:aSelector];
// 自动创建方法签名
// AnimalModel *animalModel = [AnimalModel new];
// return [animalModel methodSignatureForSelector:aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
AnimalModel *model = [AnimalModel new];
if ([model respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:model];
}else{
[self doesNotRecognizeSelector:anInvocation.selector];
}
}
解释下上面的代码:方法签名有两种方式,一个是手动创建签名,一个是自动创建签名,看代码可以明白,着重要讲的是下面这句
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
参数值"v@:"为类型编码,前面有提到,这里就不说规则了。原则上来说,该编码的类型应该与未知方法的参数对应,所以用根据未知方法自动创建方法签名更好。但是如果非要用手动创建方法签名的话,在写方法signatureWithObjCTypes:的参数值时要注意两点
- 方法的返回类型必须有,不能省略,比如这里是返回空类型,所以对应第一个编码为v
- 方法的默认参数self和_cmd对应的类型编码不能写错,固定为"@:"
满足上面两点签名就会有效,否则会导致crash,至于方法签名中参数个数与未知方法不对应是没有问题的,比如说类型编码为"v@:@@@",而未知方法为:humenName(没有参数),是不会导致crash,只不过这里的参数写多少个,会影响方法forwardInvocation:的参数值anInvocation的变化。
在第二个方法forwaidInvocation:必须判断接收者是否能响应未知消息,否则直接执行[anInvocation invokeWithTarget:model]在接收者无法响应位置消息时会导致崩溃。如果指定的接收者不能响应未知选择器,那么没办法了只能抛出异常,执行doesNotRecognizeSelector:方法,程序崩溃。
这里在讲下刚才提到的,方法签名的参数个数的问题,看下面的代码:
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if ([super methodSignatureForSelector:aSelector] == nil) {
// 手动创建
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"@@:@@"];
return signature;
}
return [super methodSignatureForSelector:aSelector];
// 自动创建方法签名
// AnimalModel *animalModel = [AnimalModel new];
// return [animalModel methodSignatureForSelector:aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
AnimalModel *model = [AnimalModel new];
SEL sel = anInvocation.selector;
NSMethodSignature *sign = anInvocation.methodSignature;
NSLog(@"%lu--%@",(unsigned long)sign.numberOfArguments,NSStringFromSelector(sel));
if ([model respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:model];
}else{
[self doesNotRecognizeSelector:anInvocation.selector];
}
}
AnimalModel类中humenName方法实现如下:
-(void)humenName{
NSLog(@"%s",__FILE__);
}
运行打印结果如下
2019-02-21 15:31:41.851617+0800 MessageTrans[14393:9247960] 4--humenName
2019-02-21 15:31:48.716371+0800 MessageTrans[14393:9247960] /Users/xiangzuhua/Desktop/MessageTrans/MessageTrans/AnimalModel.m
根据打印结果可以看到,方法签名中参数个数,与anInvocation中的参数个数值是对应的,并不与未知选择器humenName的参数个数对应(参数个数为0),而且这种不对应并不会影响AnimalModel类中方法humenName的正常执行。
拓展:如果对象调用一个未指定参数值的未知消息,但是在另一个类中有该方法的实现,会怎样呢
刚才我们调用的未知方法是没有参数的,我们实现下有参数不指定值得代码
viewController.m -> viewDidLoadHumenModel *model = [HumenModel new]; [model performSelector:@selector(humenName:)];
AnimalModel.m
-(void)humenName:(NSInteger)number{ NSLog(@"%s - %ld",__FILE__,(long)number); }
HumenModel.m中的代码和上面的相同,编译运行,发现崩溃了,原因容易想到是这个消息没有参数值,当在AnimalModel中调用humenName:时,就会报野指针。这个问题怎么解决呢,见下面的代码
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ // 自动创建方法签名 AnimalModel *animalModel = [AnimalModel new]; return [animalModel methodSignatureForSelector:aSelector]; } -(void)forwardInvocation:(NSInvocation *)anInvocation{ AnimalModel *model = [AnimalModel new]; // 给anInvocation设置参数值 NSInteger number = 10; [anInvocation setArgument:&number atIndex:2];// 为什么是2,因为0是self参数,1是_cmd参数,返回值类型不属于参数。 if ([model respondsToSelector:anInvocation.selector]) { [anInvocation invokeWithTarget:model]; }else{ [self doesNotRecognizeSelector:anInvocation.selector]; } }
这样再运行看结果如下:
2019-02-21 16:13:56.024182+0800 MessageTrans[15139:9320442] /Users/xiangzuhua/Desktop/MessageTrans/MessageTrans/AnimalModel.m - 10
运行正常
到这一步消息转发的整个流程就讲完了。
网友评论