iOS Developer

iOS 消息转发:这个锅谁背

2017-03-21  本文已影响108人  Allan_野草

一、片头

在Objective-C中,方法调用可以说成是消息发送。向一个对象发送任意一条消息都是可以的,即使类中没有实现该消息(方法)。
比如下面的rev的类中没有willCrash这个方法,向对象rev发送消息

[rev performSelector:@selector(willCrash)]

objc_msgSend(rev,@selector(willCrash))

当执行到上面的这句话时,显然会发生崩溃。
(抛出 unrecognized selector sent to instance。)

好在,像这种情况,苹果贴心地提供了一种“补全”机制,
也叫“消息转发”机制,可以让编程者有机会处理崩溃。

消息转发,通俗来讲就是找人背锅。主要走的流程,是重写NSObject的三个方法:
1、自己试下补锅:+resolveInstanceMethod:(要处理静态方法就是+resolveClassMethod:)
2、让别人来背锅:-forwardingTargetForSelector:
3、还是自己解决吧:-(void)forwardInvocation:(NSInvocation *)anInvocation

二、详解

举个例,Person类,类中只有-run方法

// Person.h
@interface Person  : NSObject
- (void)run;
@end 

// Person.m
@implememtation
- (void)run {
  NSLog(@"run");
}
@end

向这个类对象调用一个不存在的方法fly

Person *man = [Person new];
[man performSelector:@selector(fly)]; // fly并没有相应实现

下面来看看,怎么用“消息转发”,从而让它不崩溃。

1、resolveInstanceMethod(自己试下补锅)

首先来重写Person类的-resolveInstanceMethod:方法,打个断点,可以看到在程序挂掉之前该方法被回调

+ (BOOL)resolveInstanceMethod:(SEL)sel {
  BOOL didResolve;
  // do something ..
  return didResolve;
}
@implememtation
+ (BOOL)resolveInstanceMethod:(SEL)sel {
     if (sel==@selector(fly)) NSLog(@"try fly");

     // 获得方法对象
     Method impMethod = class_getInstanceMethod(self, @selector(getTwoWings));
     // 为fly方法添加getTwoWings的实现
     class_addMethod(self, @selector(fly), method_getImplementation(impMethod), method_getTypeEncoding(impMethod));

     // return YES NO都一样
     return YES;
}
- (void) getTwoWings {
      NSLog(@"get two wings and i will fly");
}
@end
// 不会发生崩溃,控制台打印 try fly
// 控制台打印 get two wings and i will fly
[man performSelector:@selector(fly)];

如果不给fly动态实现,接下来会进入“转发”

2、forwardingTargetForSelector(让别人来背锅)

“转发”会回调forwardingTargetForSelector:
把该消息转发给某个对象来处理:

- (id)forwardingTargetForSelector:(SEL)aSelector {
      id handler; // 可以处理该消息的对象
      return handler;
}
[hander performSelector:@selector(fly)];
// 实现上是 IMP ptr = class_getMethodImplementation([handler class], @selector(fly));
// if ptr == NULL {让handler处理,转发到此结束}else{还有下一步}

3、forwardInvocation(还是自己解决吧)

回调Person的-(void)forwardInvocation:(NSInvocation *)anInvocation

-(void)forwardInvocation:(NSInvocation *)anInvocation {
     if (anInvocation.selector==@selector(fly)) {
          // 相应处理,消息转发完成
     }
}

三、测试例子

实现以下的场景:
向person发送fly消息,实际上让另一个对象去调用它的fly方法

重写-forwardingTargetForSelector:,把fly消息转发给Plane类对象去处理

// Person类
@implememtation
+ (BOOL)resolveInstanceMethod:(SEL)sel {
     if (sel==@selector(fly)) NSLog(@"try fly");// 没有实现-fly,那么消息将转发
     return NO;
}
-(id)forwardingTargetForSelector:(SEL)aSelector {
      Plane *plane= [Plane new];
      return plane;// 转发给plane对象
}
@end

Plane是另外一个类:

// - Plane.h
@interface Plane : NSObject
@end 
// - Plane.m
@implememtaion Plane
-(void)fly {
      NSLog(@"take a plane and i will fly");
}
@end

测试结果

Person *man = [Person new];
[man performSelector:@selector(fly)]; 
// 控制台打印 try fly
// 控制台打印 take a plane and i will fly

相当于调用了[plane fly];

片尾

当向一个对象发送一个它不能处理的消息/方法,苹果会不断地询问你,去动态实现这个方法,简而言之,消息转发就是这么一回事。

实际应用上,消息转发配合上Runtime也有很多玩法。比如。。就不比如了,哪天写一篇文章,先挖个坑吧~

再至于流程3的forwardInvocation中,NSInvocation怎么个用法?请看另外下面这篇文章,拉到最底就能看到了
NSInvocation:iOS 不走寻常路:调用方法的6种姿势,你知道几种

End
上一篇下一篇

猜你喜欢

热点阅读