对象收到一个它无法响应的方法到崩溃;
消息转发机制共分为3大步骤:
1、动态解析。先询问接受者的类所属的类,看看有没有动态的添加方法。resolveInstanceMethod,resolveClassMethod
2、快速消息转发。如果1执行完了也没有动态新增方法,那运行期系统就请接收者看看有没有其他对象能处理这条有消息,有的话就把消息转发给哪个对象。forwardingTargetForSelector
3、完整的消息转发机制。如果2没有,那运行期系统会把消息有关的全部细节写到NSInvocation对象里面。首先签名NSMethodSignature,然后forwardInvocation处理NSInvocation对象。
第一阶段:
如果调用了对象方法首先会进行+(BOOL)resolveInstanceMethod:(SEL)sel判断
如果调用了类方法 首先会进行 +(BOOL)resolveClassMethod:(SEL)sel判断
两个方法都为类方法,如果YES则能接受消息 NO不能接受消息 进入第二步
- resolveInstanceMethod
当根据selector没有找到对应的method时,首先会调用这个方法,在该方法中你可以为一个类添加一个方法。并返回yes。下面的代码只是声明了runTo方法,没有实现。
//Car.h
@interface Car : NSObject
- (void)runTo:(NSString *)place;
@end
//Car.m
@implementation Car
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(runTo:)) {
class_addMethod(self, sel, (IMP)dynamicMethodIMPRunTo, "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
//动态添加的@selector(runTo:) 对应的实现
static void dynamicMethodIMPRunTo(id self, SEL _cmd,id place){
NSLog(@"dynamicMethodIMPRunTo %@",place);
}
@end
第二阶段:
- forwardingTargetForSelector
如果resolveInstanceMethod没有实现,返回No,或者没有动态添加方法的话,就会执行forwardingTargetForSelector。 在这里你可以返回一个能够执行这个selector的对象otherTarget,接下来消息会重新发送到这个otherTarget。
//Person.h
@interface Person : NSObject
- (void)runTo:(NSString *)place;
@end
//Person.m
@implementation Person
- (void)runTo:(NSString *)place;{
NSLog(@"person runTo %@",place);
}
@end
//Car.h
@interface Car : NSObject
- (void)runTo:(NSString *)place;
@end
//Car.m
@implementation Car
- (id)forwardingTargetForSelector:(SEL)aSelector{
//将消息转发给Person的实例
if (aSelector == @selector(runTo:)){
return [[Person alloc]init];
}
return [super forwardingTargetForSelector:aSelector];
}
@end
第三阶段
- forwardInvocation
如果上面两种情况没有执行,就会执行通过forwardInvocation进行消息转发。首先签名NSMethodSignature,然后forwardInvocation处理NSInvocation对象。
@implementation Car
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector{
//判断selector是否为需要转发的,如果是则手动生成方法签名并返回。
if (aSelector == @selector(runTo:)){
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
return [super forwardingTargetForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
//判断待处理的anInvocation是否为我们要处理的
if (anInvocation.selector == @selector(runTo:)){
}else{
}
}
@end
在NSInvocation对象中保存着我们调用一个method的所有信息。可以看下其属性和方法:
- methodSignature 含有返回值类型,参数个数及每个参数的类型 等信息。
- - (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;获取调用method时传的参数
- - (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx; 设置第index参数。
- - (void)invoke; 开始执行
- - (void)getReturnValue:(void *)retLoc; 获取返回值
下面的代码演示如何获取调用method时所传的各参数值
- (void)forwardInvocation:(NSInvocation *)anInvocation{
if (anInvocation.selector == @selector(runTo:)){
void *argBuf = NULL;
NSUInteger numberOfArguments = anInvocation.methodSignature.numberOfArguments;
for (NSUInteger idx = 2; idx < numberOfArguments; idx++) {
const char *type = [anInvocation.methodSignature getArgumentTypeAtIndex:idx];
NSUInteger argSize;
NSGetSizeAndAlignment(type, &argSize, NULL);
if (!(argBuf = reallocf(argBuf, argSize))) {
NSLog(@"Failed to allocate memory for block invocation.");
return ;
}
[anInvocation getArgument:argBuf atIndex:idx];
//现在argBuf 中保存着第index 参数的值。 你可以使用这些值进行其他处理,例如为block中各参数赋值,并调用。
}
}else{
}
}
##通过手动触发消息转发(method已经实现) 前面所描述的消息转发都是在selector没有对应实现时自动进行的,我们称之为自动消息转发。现在有个需求:即使Car类实现了 runTo:,执行[objOfCar runTo:@"shangHai"]; 时也进行消息转发(手动触发),如何实现? 实现方法如下:利用 method swizzling 将selector的实现改变为_objc_msgForward或者_objc_msgForward_stret。在调selector时就会进行消息转发。 看下面的代码:
//对 runTo: 进行消息转发
@implementation Car
//进行 method swizzling。此时调用runTo:就会进行消息转发
+ (void)load{
SEL selector = @selector(runTo:);
Method targetMethod = class_getInstanceMethod(self.class, @selector(selector));
const char *typeEncoding = method_getTypeEncoding(targetMethod);
IMP targetMethodIMP = _objc_msgForward;
class_replaceMethod(self.class, selector, targetMethodIMP, typeEncoding);
}
- (void)runTo:(NSString *)place{
NSLog(@"car runTo %@",place);
}
//消息转发,调用这个方法。anInvocation中保存着调用方法时传递的参数信息
- (void)forwardInvocation:(NSInvocation *)anInvocation{
if (anInvocation.selector == @selector(runTo:)){
}else{
}
}
应用案例:
1.JSPatch --iOS动态化更新方案
具体实现bang神已经在下面两篇博客内进行了详细的讲解,非常精妙的使用了,消息转发机制来进行JS和OC的交互,从而实现iOS的热更新。虽然去年苹果大力整改热更新让JSPatch的审核通过率在有一段时间里面无法过审,但是后面bang神对源码进行代码混淆之后,基本上是可以过审了。不论如何,这个动态化方案都是技术的一次进步,不过目前是被苹果爸爸打压的。不过如果在bang神的平台上用正规混淆版本别自己乱来,通过率还是可以的。有兴趣的同学可以看看这两篇原理文章,这里只摘出来用到消息转发的部分。
2.为 @dynamic 实现方法
使用 @synthesize 可以为 @property 自动生成 getter 和 setter 方法(现 Xcode 版本中,会自动生成),而 @dynamic 则是告诉编译器,不用生成 getter 和 setter 方法。当使用 @dynamic 时,我们可以使用消息转发机制,来动态添加 getter 和 setter 方法。当然你也用其他的方法来实现。
3.实现多重代理
利用消息转发机制可以无代码侵入的实现多重代理,让不同对象可以同时代理同个回调,然后在各自负责的区域进行相应的处理,降低了代码的耦合程度。用第三阶段实现
4.间接实现多继承
Objective-C本身不支持多继承,这是因为消息机制名称查找发生在运行时而非编译时,很难解决多个基类可能导致的二义性问题,但是可以通过消息转发机制在内部创建多个功能的对象,把不能实现的功能给转发到其他对象上去,这样就做出来一种多继承的假象。转发和继承相似,可用于为OC编程添加一些多继承的效果,一个对象把消息转发出去,就好像他把另一个对象中放法接过来或者“继承”一样。消息转发弥补了objc不支持多继承的性质,也避免了因为多继承导致单个类变得臃肿复杂。