Objective-C方法不被识别时的三次补救机会

Objective-C 的对象消息传递允许程序向没有相应方法的对象发送消息。默认情况下,如果出现这种情况,程序就会抛出运行时异常 unrecognized selector
然而Objective-C 的运行时为 unrecognized selector提供了三次补救机会:

  • 1、动态方法决议:以动态方式实现方法。NSObject+resolveClassMethod:类方法与+resolveInstanceMethod:类方法以动态方式实现由选择器指定的实例和类方法。如果动态方法决议成功,则执行IMP指向的函数;如果失败,还有第二次补救措施;
  • 2、消息转发机制:当对象收到与其方法不匹配的消息时,通过消息转发机制可以使对象执行用户预先定义的处理过程;将消息发送给能够做出回应的其它接收器;将所有无法识别的消息都发送给同一接收器;既不执行处理过程也不使程序崩溃,默默的吞下消息。Objective-C 提供了两种消息转发机制:
  • 2.1、快速转发:通过重写 NSObject- forwardingTargetForSelector:方法,将无法识别的方法转发给其它对象,从而实现快速转发
  • 2.2、完整转发:通过重写 NSObject- forwardInvocation:方法,实现完整转发。
补救措施 1、动态方法决议

以动态方式实现方法。NSObject提供了由选择器指定实现代码的操作。

//为指定的类方法选择器提供实现代码
+ (BOOL)resolveClassMethod:(SEL)sel;

//为指定的实例方法选择器提供实现代码
+ (BOOL)resolveInstanceMethod:(SEL)sel;

当找不到实现方法时,第一步先执行动态决议方法:

@implementation Model

- (void)logModel{
    NSLog(@"%s",__func__);
}

#pragma mark - 第一步:动态方法决议

void drinkingMethodIMP(id self ,SEL _cmd){
    NSLog(@"为指定的选择器 SEL : %@ 提供函数指针 IMP , 根据 IMP 执行具体的实现",NSStringFromSelector(_cmd));
}

+ (BOOL)resolveInstanceMethod:(SEL)sel{
    
    if (sel == NSSelectorFromString(@"drinking")){
        //为指定的选择器 SEL 提供函数指针 IMP:IMP 指向实现代码
        return class_addMethod(self.class, sel, (IMP)drinkingMethodIMP, "v@:@");
    }
    
    //去父类判断:是否父类提供了 IMP
    BOOL superResult = [super resolveInstanceMethod:sel];
    NSLog(@"step_1 : %s  |  selector : %@   |  superResult : %d",__func__,NSStringFromSelector(sel),superResult);
    return superResult;
}

@end
测试动态方法决议
int main(int argc, char * argv[]) {
    @autoreleasepool {
        id model = [[Model alloc] init];
        [model performSelector:@selector(logModel)];
        [model performSelector:@selector(drinking)];
    }
    return 0;
}

执行上述代码,程序正常运行,没有 unrecognized selector sent to instance的异常,获取打印日志:

-[Model logModel]
为指定的选择器 SEL : drinking 提供函数指针 IMP , 根据 IMP 执行具体的实现

实例方法-logModel正常执行,但是 -drinking被指向了 drinkingMethodIMP()函数。

我们再来回忆下 Objective-C 的方法 Method的本质,它的实现代码从本质来说是结构体objc_method的成员IMP指向的函数;

在动态决议中,我们通过class_addMethod()函数为指定的选择器sel提供IMP,因此程序不会异常闪退;

/* Runtime 库提供的 C 语言函数:为一个指定类添加方法
 * @param cls 指定的类
 * @param name 选择器
 * @param imp 函数指针
 * @param types 描述方法参数类型的字符数组
 *
 * 说明:参数 name 、imp、types 是方法Method的结构体objc_method 的三个成员
 * 该函数将添加超类实现的重写,但不会替换该类中的现有实现。
 * 也就是说:如果该类没有实现选择器指定的方法,则添加成功,返回YES;
 *         如果该类已经实现选择器指定的方法,则添加失败,返回 NO;
 * 需要更改现有的实现,使用 method_setImplementation()函数。
 */
BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types) 
补救措施 2、快速转发(重定向接收者)

通过重写 NSObject- forwardingTargetForSelector:方法,将无法识别的方法转发给其它对象,从而实现快速转发。

//消息转发辅助类
@interface ModelHelper : NSObject
- (void)eatFood;
@end

@implementation ModelHelper

- (void)eatFood{
    NSLog(@"%s",__func__);
}

@end

@interface Model : NSObject
@property (nonatomic ,strong) ModelHelper *helper;
@end

@implementation Model

#pragma mark - 第二步:重定向接收者

/* 重定向接收者:当一个对象无法找到消息时,为该消息提供一个能够处理它的对象
 *
 * 如果动态方法决议没有提供实现的函数或者提供失败,就会执行该方法重定向接收者;
 * 注意:不能提供 self,否则进入死循环
 */
- (id)forwardingTargetForSelector:(SEL)aSelector{
    
    //检查消息 aSelector 能否由 ModelHelper 实例处理:如果能处理,那么返回 ModelHelper 实例
    if ([self.helper respondsToSelector:aSelector]) {
        return self.helper;
    }

    //如果不能处理,那么去父类判断:是否父类重定向接收者
    id superResult = [super forwardingTargetForSelector:aSelector];
    NSLog(@"step_2 : %s  |  selector : %@   |  superResult : %@",__func__,NSStringFromSelector(aSelector),superResult);
    return superResult;
}

//懒加载一个消息转发辅助类
- (ModelHelper *)helper{
    if (_helper == nil) {
        _helper = [[ModelHelper alloc] init];
    }
    return _helper;
}

@end
测试快速转发功能:
int main(int argc, char * argv[]) {
    @autoreleasepool {
        id model = [[Model alloc] init];
        [model performSelector:@selector(eatFood)];
    }
    return 0;
}

执行上述代码,程序正常运行,没有 unrecognized selector sent to instance的异常,获取打印日志:

step_1 : +[Model resolveInstanceMethod:]  |  selector : eatFood   |  superResult : 0
-[ModelHelper eatFood]

没有实现的实例方法 -eatFood首先执行动态方法决议,动态方法决议返回 NO,也就是补救失败了;接着重定向接收者,实例方法 -eatFood被转发给ModelHelper实例执行。

我们知道 Objective-C 消息封装为NSInvocation类,该类包含Objective-C消息的所有元素:目标target、选择器SEL、参数和返回值。
也就是说;重定向接收者本质来讲就是调用了NSInvocation- invokeWithTarget:实例方法,指定目标并再次发送消息。重新执行消息传递过程的几个步骤。

补救措施 3、完整转发

完整转发是相对于快速转发来说的:快速转发的本质是重定向接收者,调用NSInvocation- invokeWithTarget:方法向指定目标发送消息。

#pragma mark - 第三步:完整消息转发

//获取方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if ([ModelHelper instancesRespondToSelector:aSelector]){
        //从消息转发辅助类 获取 方法签名
        NSMethodSignature *helperSign = [ModelHelper instanceMethodSignatureForSelector:aSelector];
        NSLog(@"step_3_1 : %s  |  selector : %@   |  helperSign : %@",__func__,NSStringFromSelector(aSelector),helperSign);
        return helperSign;
    }
    
    //尝试从父类获取方法签名:如果父类没有实现该方法,则返回 null
    NSMethodSignature *superSign = [super methodSignatureForSelector:aSelector];
    NSLog(@"step_3_2 : %s  |  selector : %@   |  superResult : %@",__func__,NSStringFromSelector(aSelector),superSign);
    return superSign;
}

/* 未知消息分发中心,将未知消息转发给其它对象
 * 只有在消息接收对象无法正常响应消息时才被调用
 */
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSLog(@"step_3_3 : %s  |  target : %@  |  selector : %@",__func__,anInvocation.target,NSStringFromSelector(anInvocation.selector));
    NSLog(@"step_3_3 : NSMethodSignature : %@",anInvocation.methodSignature);
    
    if ([ModelHelper instancesRespondToSelector:anInvocation.selector]){
        [anInvocation invokeWithTarget:self.helper];
    }
}
测试完整转发功能:
int main(int argc, char * argv[]) {
    @autoreleasepool {
        id model = [[Model alloc] init];
        [model performSelector:@selector(playGames)];
    }
    return 0;
}

执行上述代码,程序正常运行,没有 unrecognized selector sent to instance的异常,获取打印日志:

step_1 : +[Model resolveInstanceMethod:]  |  selector : playGames   |  superResult : 0
step_2 : -[Model forwardingTargetForSelector:]  |  selector : playGames   |  superResult : (null)
step_3_1 : -[Model methodSignatureForSelector:]  |  selector : playGames   |  helperSign : 
step_1 : +[Model resolveInstanceMethod:]  |  selector : _forwardStackInvocation:   |  superResult : 0
step_1 : +[Model resolveInstanceMethod:]  |  selector : encodeWithOSLogCoder:options:maxLength:   |  superResult : 0
step_3_3 : -[Model forwardInvocation:]  |  target :   |  selector : playGames
step_3_3 : NSMethodSignature : 
-[ModelHelper playGames]

Runtime 没有找到 Model实例的-playGames方法

  • 首先执行动态方法决议,动态方法决议失败;
  • 接着重定向接收者,没有找到能够处理该消息的对象;
  • 然后通过选择器类型SEL获取消息转发辅助类ModelHelper的指定方法签名 NSMethodSignature,将这个方法签名赋值给该消息NSInvocation
  • 最后在消息转发中心判断上步骤中出现的消息转发辅助类能否响应该方法;如果可以,调用NSInvocation- invokeWithTarget:方法向消息转发辅助类实例发送消息。

你可能感兴趣的:(Objective-C方法不被识别时的三次补救机会)