3.拯救未知消息的3根救命稻草
第一根救命稻草:
如上所说,如果对象整个继承链都无法处理当前消息,那么首先会调用接收对象所属类的resolveInstanceMethod方法(这个对应实例方法,如果是无法处理的类方法消息,则会调用resolveClassMethod方法),在这个方法中,开发者有机会为类动态添加方法,如果动态添加了方法,可以在这个方法中返回YES,那么此条消息依然会被成功处理。例如我们将main.m文件修改如下:
#import <Foundation/Foundation.h>
#import "MyObject.h"
#import <objc/message.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
MyObject * obj = [[MyObject alloc]init];
[obj class];
//为了消除未定义选择器的警告
#pragma clang diagnostic push
#pragma clang diagnostic ignored"-Wundeclared-selector"
//进行消息发送
((void(*)(id,SEL))objc_msgSend)(obj,@selector(showSelf));
#pragma clang diagnostic pop
}
return 0;
}
MyObject类不做任何修改,当我们运行程序,程序会直接crash掉,现在我们在MyObject类中添加如下方法:
+(BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"resolveInstanceMethod");
if ([NSStringFromSelector(sel) isEqualToString:@"showSelf"]) {
class_addMethod(self, sel, newFunc, "v@:");
}
return [super resolveInstanceMethod:sel];
}
其中class_addMethod函数用来向类中动态添加方法,第一个参数为Class对象,第二个参数为方法选择器,第三个参数为IMP类型的函数指针,第四个参数为指定方法的返回值和参数类型。这个参数采用的是C字符串的形式来指定返回值和参数的类型,第1个字符为返回值类型,其后都为参数类型,需要注意,使用这种方式添加方法的时候系统会默认传入两个参数,分别是调用此方法的实例对象和方法选择器,上面示例代码中的"@"表示第1个id类型的参数,":"表示第2个选择器类型的参数,后面我会把字符所表示的参数类型映射表提供给大家。
抽丝剥茧一下,IMP和SEL并不同,SEL可以理解为函数签名,其与函数名相关联,而IMP是函数所在地址的指针,其定义如下:
typedef void (*IMP)(void /* id, SEL, ... */ );
简单理解,通过IMP我们可以直接拿到函数的地址,后面会对函数做更深入的剖析,到时候你能就能豁然你开朗。
运行工程,根据打印信息可以看到showSelf方法被添加并正常执行了。
第二根救命稻草:
抛开运行时添加方法这一手段,将resolveInstanceMethod方法删去,是不是我们的程序就必然走进crash的深渊了,其实不然,上帝还会给你另一根救命稻草,当通过运行时添加方法被否定后,系统会接着调用forwardingTargetForSelector方法,这个方法用来对消息进行转发,没错,重点来了,Objective-C中强大的消息转发机制的奥妙就在这里。forwardingTargetForSelector方法需要返回一个id类型的对象,系统会将当前对象服务处理的消息转发给这个方法返回的对象,如果这个返回的对象可以处理,那么程序依然可以很好的执行下去。
例如,在我们的命令行工程中新添加一个类,命名为SubObject,实现如下:
SubObject.h文件:
#import <Foundation/Foundation.h>
@interface SubObject : NSObject
@end
SubObject.m文件:
#import "SubObject.h"
@implementation SubObject
-(void)showSelf{
NSLog(@"subObject");
}
@end
在MyObject类中实现如下方法:
-(id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"forwardingTargetForSelector");
if ([NSStringFromSelector(aSelector) isEqualToString:@"showSelf"]) {
return [SubObject new];
}
return [super forwardingTargetForSelector:aSelector];
}
forwardingTargetForSelector方法可以返回一个对象,Objective-C会将当前对象无法处理的消息转发给这个方法返回的对象,如果返回nil,则表示不进行消息转发,这时你如果还想挽救此次crash,你就需要用到第三根救命稻草了。我们可以这种消息转发的机制来模拟Objective-C中的多继承。
第三根救命稻草:
如果你不幸错过了前两次拯救未知消息的机会,那么你还有最后一次机会(中国有句古话,事不过三,世间万事也果真如此...)。当消息转发策略也被否定后,系统会调用methodSignatureForSelector方法,这个方法的主要用途是询问这个选择器是否是有效的,我们需要返回一个NSMethodSignature,顾名思义,这个对象是函数签名的抽象。如果我们返回了有效的函数签名,那么接着系统会调用forwardInvocation方法,这里是拯救应用程序的最后一根稻草了,这个函数会直接将消息包装成NSInvocation对象传入,我们直接将其发送给可以处理此消息的对象即可(当然你也可以直接抛弃,不理会这条未知的消息)。
例如,在MyObject类中将forwardingTargetForSelector方法删去,实现如下两个方法:
//询问此选择器是否是有效的
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"methodSignatureForSelector");
if ([NSStringFromSelector(aSelector) isEqualToString:@"showSelf"]) {
return [[SubObject new] methodSignatureForSelector:aSelector];
}
return [super methodSignatureForSelector:aSelector];
}
//处理消息
-(void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"forwardInvocation");
if ([NSStringFromSelector(anInvocation.selector) isEqualToString:@"showSelf"]) {
[anInvocation invokeWithTarget:[SubObject new]];
}else{
[super forwardInvocation:anInvocation];
}
}
再次运行工程,程序又被你挽救了一次。
你真的需要救命稻草么?
通过上面的三根救命稻草,我相信你一定对Objective-C消息机制有了全面而深入的了解,上面的代码也只是为了示例所用,正常情况下,你都不会使用到这些函数(毕竟如果你需要救命稻草,说明你已经落水了)。除非某些特殊需求或者做一些调试框架的开发,否则尽量不要介入消息的发送机制,就像生病就医,发现问题总比逃避治疗要好。顺便说一下,如果你没有使用任何救命稻草,当向某个对象发送了无法处理的消息时,系统会最终调用到NSObject类的doesNotRecognizeSelector方法,这个方法会抛出异常信息,正因如此,你在Xcode的控制台会经常看到如下图所示的crash信息:
你也可以重写这个方法来自定义输出信息,例如:
-(void)doesNotRecognizeSelector:(SEL)aSelector{
NSLog(@"doesNotRecognizeSelector");
if ([NSStringFromSelector(aSelector) isEqualToString:@"showSelf"]) {
NSLog(@"not have a method named showSelf");
return;
}
[super doesNotRecognizeSelector:aSelector];
}
下图完整展示了Objective-C整个消息发送与转发机制: