OC Runtime之动态方法解析和消息转发
转发:链接
首先直接上代码:
@interface Person : NSObject
- (void)eat;
- (void)say:(NSString*)world;
@end
@implementation AA
- (void)eat {
NSLog(@"eat");
}
@end
int main (int argc, const charchar * argv[]) {
@autoreleasepool {
Person * p = [Person new];
[p eat];
[p say:@”Hello”];
}
return 0;
}
以上代码执行后会产生什么样的结果?我想大家都应该很清楚,那就是程序会crash,因为我们并未实现方法say:,在Person及其父类的method list中都找不到其相应实现,所以会crash。错误原因:-[Person say:]: unrecognized selector sent to instance 0x100100020
那么怎么解决这样的问题呢,答案就是动态方法解析(Dynamic Method Resolution)和消息转发(Message Forwarding)。
动态方法解析(Dynamic Method Resolution)
你可以动态地提供一个方法的实现。例如我们可以用@dynamic关键字在类的实现文件中修饰一个属性:
@dynamic propertyName;
关键字dynamic的作用是告诉编译器与属性相关的方法将在运行时动态提供,编译器不需生成对应的getter
setter方法(原文:tells the compiler that the methods associated with the
property will be provided
dynamically)。我们可以通过分别重载resolveInstanceMethod:和resolveClassMethod:方法分别添加实例方法实现和类方法实现。因为当
Runtime
系统在Cache和方法分发表中(包括超类)找不到要执行的方法时,Runtime会调用resolveInstanceMethod:或resolveClassMethod:来给程序员一次动态添加方法实现的机会。我们需要用class_addMethod函数完成向特定类添加特定方法实现的操作:
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end
PS:动态方法解析会在消息转发机制浸入前执行。如果 respondsToSelector: 或
instancesRespondToSelector:方法被执行,动态方法解析器将会被首先给予一个提供该方法选择器对应的IMP的机会。如果你实想让该方法选择器通过转发机制转发,那么就让resolveInstanceMethod:返回NO。
给某个对象发送无法处理的消息时会产生错误。幸运的是在错误报出之前,runtime系统给了这个对象第二次处理这个消息的机会(the
runtime system gives the receiving object a second chance to handle the
message)。
重定向
在消息转发机制执行前,Runtime 系统会再给我们一次偷梁换柱的机会,即通过重载- (id)forwardingTargetForSelector:(SEL)aSelector方法替换消息的接受者为其他对象:
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}
如果此方法返回nil或self,则会进入消息转发机制(forwardInvocation:);否则将向返回的对象重新发送消息。
转发
当动态方法解析不作处理返回NO时,消息转发机制会被触发。在这时forwardInvocation:方法会被执行,我们可以重写这个方法来定义我们的转发逻辑:
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
该消息的唯一参数是个NSInvocation类型的对象——该对象封装了原始的消息和消息的参数。我们可以实现forwardInvocation:方法来对不能处理的消息做一些默认的处理,也可以将消息转发给其他对象来处理,而不抛出错误。
这里需要注意的是参数anInvocation是从哪的来的呢?其实在forwardInvocation:消息发送前,Runtime系统会向对象发送methodSignatureForSelector:消息,并取到返回的方法签名用于生成NSInvocation对象。所以我们在重写forwardInvocation:的同时也要重写methodSignatureForSelector:方法,否则会抛异常。
当一个对象由于没有相应的方法实现而无法响应某消息时,运行时系统将通过forwardInvocation:消息通知该对象。每个对象都从NSObject类中继承了forwardInvocation:方法。然而,NSObject中的方法实现只是简单地调用了doesNotRecognizeSelector:。通过实现我们自己的forwardInvocation:方法,我们可以在该方法实现中将消息转发给其它对象。
forwardInvocation:方法就像一个不能识别的消息的分发中心,将这些消息转发给不同接收对象。或者它也可以象一个运输站将所有的消息都发送给同一个接收对象。它可以将一个消息翻译成另外一个消息,或者简单的”吃掉“某些消息,因此没有响应也没有错误。forwardInvocation:方法也可以对不同的消息提供同样的响应,这一切都取决于方法的具体实现。该方法所提供是将不同的对象链接到消息链的能力。
注意: forwardInvocation:方法只有在消息接收对象中无法正常响应消息时才会被调用。
所以,如果我们希望一个对象将negotiate消息转发给其它对象,则这个对象不能有negotiate方法。否则,forwardInvocation:将不可能会被调用。
整个消息处理过程如下图所示:
步骤1、2、3都是在没有实现msg方法的情况下,runtime为我们提供的补救的机会。
针对文章开始崩溃的问题,可以用三种方式补救:
第一种:resolveInstanceMethod:
// c形式
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(say:)) {
class_addMethod([self class], sel, (IMP)say, "v@:*");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void say(id self, SEL _cmd, NSString *str) {
NSLog(@"Person say:%@", str);
}
//OC形式
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(say:)) {
class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(sayMethodIMP:)), "v@:*");
return YES;
}
return [super resolveInstanceMethod:sel];
}
- (void)sayMethodIMP:(NSString *)str {
NSLog(@"Person say:%@", str);
}
第二种:forwardingTargetForSelector:这里增加了Mobile类来相应say:消息。
//Mobile.h
#import <Foundation/Foundation.h>
@interface Mobile : NSObject
- (void)say:(NSString*)world;
@end
//Mobile.m
#import "Mobile.h"
@implementation Mobile
- (void)say:(NSString*)world {
NSLog(@"Mobile say:%@", world);
}
@end
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(say:)) {
return [Mobile new];
}
return [super forwardingTargetForSelector:aSelector];
}
第三种:forwardInvocation:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(say:)) {
return [NSMethodSignature signatureWithObjCTypes:"V@:*"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL selector = [anInvocation selector];
Mobile *mobile = [Mobile new];
if ([mobile respondsToSelector:selector]) {
[anInvocation invokeWithTarget:mobile];
}
}