美文网首页性能调优、测试
unrecognized selector异常捕获

unrecognized selector异常捕获

作者: jayhe | 来源:发表于2020-07-07 20:19 被阅读0次

在开发的过程中我们会遇到各种各样的异常,unrecognized selector当调用方法的时候,消息接收者无法响应函数,或者消息转发流程也无法处理的时候,系统就会抛出异常,程序闪退

1. 常见场景

1.1 调用了对象未实现的方法

该场景是定义了函数,但是却未实现,编译器也会给一个警告Method definition for 'testMethodNotImp' not found

@interface ViewController ()

- (void)testMethodNotImp;

@end

// 测试代码
[self testMethodNotImp];

执行之后会产生如下的异常:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ViewController testMethodNotImp]: unrecognized selector sent to instance 0x7fe82be0a320'

此时可以将编译器的警告提升到error级别,在开发阶段让开发去解决规避

在Xcode-Build Settings -- Other Warning Flags
-- 加上Werror=xxx;至于这个警告的选项,我们可以去查阅clang的文档去找,也可以查看Xcode的build日志找到对应的文件就可以看到

这里介绍如何通过Xcode来查看
查看Xcode的Build日志:


图片.png

展开找到警告的信息,可以得到警告的编译选项[-Wincomplete-implementation]

图片.png

此时我们需要将warning提升为警告-Werror=incomplete-implementation并添加到Other Warning Flags中去

图片.png

这样就将警告提升为error了,在开发阶段强制开发去处理该类型警告了

1.2 performSelector执行了对象没有的方法

比如:对tableView通过performSelector去执行一个没有的方法haha;此时编译器也会给出警告Undeclared selector 'haha'

[self.entryTableView performSelector:@selector(haha)];

增加编译选项-Werror=undeclared-selector

1.3 父类调用了子类的方法

比如定义的是NSMutableArray,结果赋值的时候给了个NSArray,此时调用了NSMutableArray的方法

NSMutableArray *array = [NSArray array];
[array removeLastObject];

此时会编译器会报警告Incompatible pointer types initializing 'NSMutableArray *' with an expression of type 'NSArray * _Nonnull'然而这个警告却带来的是程序异常

增加编译选项 -Werror=incompatible-pointer-types

往往很多异常就是我们忽略了警告的处理,我们将其提升为Error级别来强制开发去解决;但是针对这个不匹配的类型的警告,有时候又是我们可以接受的已知的,针对这种场景我们也可以针对性的忽略掉该警告,如下所示:

- (void)logAllCachedData {
    SEL allObjects = NSSelectorFromString(@"allObjects");
    IMP (*cachedAllObjects)(id, SEL) = NULL;
    if ([self.memoryCache respondsToSelector:allObjects]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wincompatible-pointer-types"
        cachedAllObjects = class_getMethodImplementation([self.memoryCache class], allObjects);
#pragma clang diagnostic pop
    }
    if (cachedAllObjects != NULL) {
        NSLog(@"%@", cachedAllObjects(self.memoryCache, allObjects));
    }
}

1.4 接口返回数据格式跟定义不一样的场景

经常我们会遇到接口定义的字段是NSString类型,结果接口返回Number类型,如果代码写的不够健壮没有去判断isKindOfClass则会异常

{
     NSString *testString;
     id serverData = [NSNumber numberWithInt:12345];
     testString = serverData;
      __unused NSInteger length = testString.length;
}

这种场景可以在处理接口返回的数据时,都做一次isKindOfClass的判断,或者使用消息转发去处理,后面会介绍到

1.5 野指针异常

野指针异常一般就是访问了非法内存,常见的就是代理用assign修饰、向已经释放的对象发送消息、观察者未移出等

@interface ViewController ()

@property (nonatomic, assign) UILabel *testLabel;

@end

- (void)viewDidLoad {
    [super viewDidLoad];
    self.testLabel = [UILabel new];
    self.testLabel.text = @"12345";
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"xxxxxxxxxx");
    NSLog(@"%@", self.testLabel.text);
}

异常如下:

2020-07-07 17:58:23.525547+0800 LearningTest[83227:6276909] xxxxxxxxxx
2020-07-07 17:58:23.525790+0800 LearningTest[83227:6276909] -[NSConcreteMapTable text]: unrecognized selector sent to instance 0x7fdeae403d70
2020-07-07 17:58:23.531953+0800 LearningTest[83227:6276909] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSConcreteMapTable text]: unrecognized selector sent to instance 0x7fdeae403d70'

添加编译选项-Werror=arc-unsafe-retained-assign
将代理使用weak修饰

2.线上异常捕获方案

  • 我们通过编译配置提升warning为error可以规避一些问题,但是线上也会由于数据等等问题出现unrecognized selector的异常
  • 有些场景我们希望可以在Release模式下捕获掉这个异常,Debug模式下则抛出异常
  • Release模式下如果捕获了异常则上报日志到日志系统

实现方案

  • 1.hook消息转发流程,对于可以捕获的对象则捕获该异常,执行一个空函数
  • 2.对于数据格式不对导致的异常,可以针对性的对某些数据类型进行forwardingTargetForSelector;例如:NSNumber可以forward到NSString
  • 3.参照系统的异常handler设置,也提供异常处理的函数供外部设置
  • 4.捕获到异常的时候,如果设置了异常handler则回调异常信息
  • 5.为了增加可配置行,将是否捕获异常和forward的逻辑通过协议的形式定义出来,业务层可以根据需要实现协议

上代码
协议定义如下:

@protocol RLCatchUnrecognizedSelectorProtocol <NSObject>

@optional
/// 默认是YES:不实现则当作YES
- (BOOL)shouldCatch;

/// 对象方法找不到的时候可以转发的targets
/// eg:接口返回的数据定义的是String,但是返回的是Number类型,假如没有做isKindOf的校验,去直接判断length的话就异常了;这时候我们可以采用forward的方式,来避免这个异常
- (NSArray<NSObject *> *)targetsToForward;

@end

CATCH_CRASH可以在Debug设置为1,在Release设置为0来实现对发布版本才捕获异常

RLSetUnrecognizedSelectorExceptionHandler供外部设置异常处理回调

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

/**
 在release模式下,当收到unrecognized selector异常的时候,转发selector为nilMessage,防止闪退
 */
@interface NSObject (RLSafe)

@end

/*
 这里也可以参照系统的返回一个NSException对象,可以包含调用的堆栈信息;
 往往项目中如果自己处理这些信息的话,会按照自己的格式去采集异常堆栈信息,这里就返回一个函数的描述;
 可以自定义handler内部再采集异常堆栈信息然后按照格式拼装数据上传
 */
typedef void RLUnrecognizedSelectorExceptionHandler(NSDictionary<NSString *, NSString *> *unrecognizedSelectorInfo);
FOUNDATION_EXPORT NSString const *RLUnrecognizedSelectorMessageKey; // unrecognizedSelectorInfo对应的key
FOUNDATION_EXPORT NSString const *RLForwardTargetMessageKey; // unrecognizedSelectorInfo对应的key

FOUNDATION_EXPORT RLUnrecognizedSelectorExceptionHandler * _Nullable RLGetUnrecognizedSelectorExceptionHandler(void);
FOUNDATION_EXPORT void RLSetUnrecognizedSelectorExceptionHandler(RLUnrecognizedSelectorExceptionHandler * _Nullable);

NS_ASSUME_NONNULL_END

hook forwardingTargetForSelector内部判断是否实现了targetsToForward协议方法来转发消息;没有实现再读取默认的设置(默认的设置主要是对基础数据做了处理)

hook methodSignatureForSelector内部判断是否实现了shouldCatch协议方法来读取是否需要捕获异常

hook forwardInvocation内部判断外部是否设置了异常handler,如果有设置就回调

#import "NSObject+RLSafe.h"
#import "MethodSwizzleUtil.h"
#import <objc/runtime.h>

static RLUnrecognizedSelectorExceptionHandler *RLUSEHandler = NULL;
NSString const *RLUnrecognizedSelectorMessageKey = @"RLUnrecognizedSelectorMessageKey";
NSString const *RLForwardTargetMessageKey = @"RLForwardTargetMessageKey";

RLUnrecognizedSelectorExceptionHandler * _Nullable RLGetUnrecognizedSelectorExceptionHandler(void) {
    return RLUSEHandler;
}

void RLSetUnrecognizedSelectorExceptionHandler(RLUnrecognizedSelectorExceptionHandler * _Nullable handler) {
    RLUSEHandler = handler;
}

@implementation NSObject (RLSafe)

+ (void)load {
#if CATCH_CRASH
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [MethodSwizzleUtil swizzleInstanceMethodWithClass:[NSObject class] originalSel:@selector(forwardInvocation:) replacementSel:@selector(rl_forwardInvocation:)];
        [MethodSwizzleUtil swizzleInstanceMethodWithClass:[NSObject class] originalSel:@selector(methodSignatureForSelector:) replacementSel:@selector(rl_methodSignatureForSelector:)];
        [MethodSwizzleUtil swizzleInstanceMethodWithClass:[NSObject class] originalSel:@selector(forwardingTargetForSelector:) replacementSel:@selector(rl_forwardingTargetForSelector:)];
    });
#endif
}

- (id)rl_forwardingTargetForSelector:(SEL)aSelector {
#if CATCH_CRASH
    NSArray<NSObject *> *targets;
    if ([self conformsToProtocol:@protocol(RLCatchUnrecognizedSelectorProtocol)] && [self respondsToSelector:@selector(targetsToForward)]) {
        targets = [(id<RLCatchUnrecognizedSelectorProtocol>)self targetsToForward];
    } else {
        targets = [self commonDataTargetsToForward]; // 默认的转发,只处理部分基础数据的
    }
    if (targets) {
        __block NSObject *targetToForward = nil;
        [targets enumerateObjectsUsingBlock:^(NSObject * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            if ([obj respondsToSelector:aSelector]) {
                targetToForward = obj;
                *stop = YES;
            }
        }];
        if (targetToForward && RLUSEHandler) {
            NSString *info = [NSString stringWithFormat:@"instance %p -[%@ %@] forward target to %@", self, self.class, NSStringFromSelector(aSelector), targetToForward.class];
            NSDictionary *userInfo = @{
                RLForwardTargetMessageKey : info
            };
            RLUSEHandler(userInfo);
        }
        return targetToForward;
    } else {
        return [self rl_forwardingTargetForSelector:aSelector];
    }
#else
    return [self rl_forwardingTargetForSelector:aSelector];
#endif
}

- (NSMethodSignature *)rl_methodSignatureForSelector:(SEL)selector {
#if CATCH_CRASH
    BOOL shouldCatch = YES;
    if ([self conformsToProtocol:@protocol(RLCatchUnrecognizedSelectorProtocol)] && [self respondsToSelector:@selector(shouldCatch)]) {
        shouldCatch = [(id<RLCatchUnrecognizedSelectorProtocol>)self shouldCatch];
    }
    if (shouldCatch) {
        NSMethodSignature *signature = [self rl_methodSignatureForSelector:selector];
        if (signature == nil) {
            signature = [self methodSignatureForSelector:@selector(nilMessage)];
        }
        
        return signature;
    } else {
        return [self rl_methodSignatureForSelector:selector];
    }
#else
    return [self rl_methodSignatureForSelector:selector];
#endif
}

- (void)rl_forwardInvocation:(NSInvocation *)anInvocation {
#if CATCH_CRASH
    if (![self respondsToSelector:anInvocation.selector]) {
        if (RLUSEHandler) {
            NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
            NSString *message = [NSString stringWithFormat:@"-[%@ %@]: unrecognized selector sent to instance %p", NSStringFromClass(((NSObject *)anInvocation.target).class), NSStringFromSelector(anInvocation.selector), anInvocation.target];
            [userInfo setObject:message forKey:RLUnrecognizedSelectorMessageKey];
            
            RLUSEHandler(userInfo);
        }
        anInvocation.selector = @selector(nilMessage);
        [anInvocation invokeWithTarget:self];
        return;
    }
#else
    [self rl_forwardInvocation:anInvocation];
#endif
}

#pragma mark - Private Method

- (id)nilMessage {
    return nil;
}

- (nullable NSArray<NSObject *> *)commonDataTargetsToForward {
    if ([self isKindOfClass:[NSString class]] ||
        [self isKindOfClass:[NSArray class]] ||
        [self isKindOfClass:[NSDictionary class]] ||
        [self isKindOfClass:[NSData class]]) {
        static NSArray<NSObject *> *dataTargets;
        if (dataTargets == nil) {
            dataTargets = @[
                [NSString string],
                [NSArray array],
                [NSDictionary dictionary],
                [NSData data],
            ];
        }
        return dataTargets;
    } else if ([self isKindOfClass:[NSNumber class]]) {
        static NSArray<NSObject *> *numberTargets;
        if (numberTargets == nil) {
            numberTargets = @[
                [NSString string]
            ];
        }
    }
    return nil;
}

@end

测试代码

void MineHandler(NSDictionary<NSString *, NSString *> *unrecognizedSelectorInfo) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wincompatible-pointer-types"
    //NSLog(@"%@", [unrecognizedSelectorInfo objectForKey:RLUnrecognizedSelectorMessageKey]);
#pragma clang diagnostic pop
    NSLog(@"%@", unrecognizedSelectorInfo);
}

- (void)testCatchUnrecognizedSelector {
    RLSetUnrecognizedSelectorExceptionHandler(&MineHandler);
    [self testMethodNotImp];
    [self.entryTableView performSelector:@selector(haha)];
    {
        NSMutableArray *array = [NSArray array];
        [array removeLastObject];
    }
    {
        NSString *testString;
        id serverData = [NSNumber numberWithInt:12345];
        testString = serverData;
        __unused NSInteger length = testString.length;
    }
}

异常捕获住了,输出日志;我们可以在异常回调里面将堆栈信息一起打包上传到日志采集系统

2020-07-07 20:08:37.990537+0800 RuntimeLearning[83939:6307462] {
    RLUnrecognizedSelectorMessageKey = "-[ViewController testMethodNotImp]: unrecognized selector sent to instance 0x7ff8fad07ba0";
}
2020-07-07 20:08:38.935344+0800 RuntimeLearning[83939:6307462] {
    RLUnrecognizedSelectorMessageKey = "-[UITableView haha]: unrecognized selector sent to instance 0x7ff8fc039000";
}
2020-07-07 20:08:39.695369+0800 RuntimeLearning[83939:6307462] {
    RLUnrecognizedSelectorMessageKey = "-[__NSArray0 removeLastObject]: unrecognized selector sent to instance 0x7fff80617ad0";
}
2020-07-07 20:08:40.377277+0800 RuntimeLearning[83939:6307462] {
    RLUnrecognizedSelectorMessageKey = "-[__NSCFNumber length]: unrecognized selector sent to instance 0xf3b018852f8da01d";
}

3.总结

  • 我们可以在debug模式让问题暴露出来,在release模式可以做一些处理,捕获住一些场景,让程序不退出
  • 同时在捕获异常的时候,上传异常日志,便于分析解决问题
  • 捕获了异常可能会带来逻辑的问题,所以建议谨慎使用,重要的业务流程出了问题还是建议该异常就异常

相关文章

网友评论

    本文标题:unrecognized selector异常捕获

    本文链接:https://www.haomeiwen.com/subject/taizqktx.html