在开发的过程中我们会遇到各种各样的异常,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日志:

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

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

这样就将警告提升为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模式可以做一些处理,捕获住一些场景,让程序不退出
- 同时在捕获异常的时候,上传异常日志,便于分析解决问题
- 捕获了异常可能会带来逻辑的问题,所以建议谨慎使用,重要的业务流程出了问题还是建议该异常就异常
网友评论