iOS编程中的常见奔溃汇总
1、找不到方法的实现unrecognized selector sent to instance
2、KVC造成的crash
3、EXC_BAD_ACCESS
4、KVO引起的崩溃
5、集合类相关崩溃
6、多线程中的崩溃
7、Socket长连接,进入后台没有关闭
8、Watch Dog超时造成的crash
9、后台返回NSNull导致的崩溃,多见于Java做后台服务器开发语言
1、找不到方法的实现unrecognized selector sent to instance
1.1、场景对应的Code
#import "UnrecognizedSelectorVC.h"/**
代理协议
*/
@protocol UnrecognizedSelectorVCDelegate@optional
- (void)notImplementionFunc;
@end
/**
测试控制器的代理对象
*/
@interface UnrecognizedSelectorVCObj : NSObject@property (nonatomic, strong) NSString *name;
@end
@implementation UnrecognizedSelectorVCObj
@end
/**
测试控制器
*/
@interface UnrecognizedSelectorVC ()
@property(nonatomic, weak) iddelegate;
@property(nonatomic, copy) NSMutableArray *mutableArray;
@end
@implementation UnrecognizedSelectorVC
- (void)viewDidLoad {
[super viewDidLoad];
[self case1];
}
/**
场景一:没有实现代理
*/
- (void)case1 {
UnrecognizedSelectorVCObj* obj = [[UnrecognizedSelectorVCObj alloc] init];
self.delegate = obj;
// 崩溃:reason: '-[UnrecognizedSelectorVCObj notImplementionFunc]: unrecognized selector sent to instance 0x2808047f0'[self.delegate notImplementionFunc];
// 解决办法:应该使用下面的代码if ( [self.delegate respondsToSelector:@selector(notImplementionFunc)] ) {
[self.delegate notImplementionFunc];
}
}
/**
场景二:可变属性使用copy修饰
*/
- (void)case2 {
NSMutableArray* array = [NSMutableArray arrayWithObjects:@1, @2, @3, nil];
self.mutableArray = array;
// 崩溃:reason: '-[__NSArrayI addObject:]: unrecognized selector sent to instance 0x281198a50'[self.mutableArray addObject:@4];
// 原因:NSMutableArray经过copy之后变成NSArray
// @property (nonatomic, copy) NSMutableArray *mArray;
// 等同于
// - (void)setMArray:(NSMutableArray *)mArray {
// _mArray = mArray.copy;
//}
// 解决办法:使用strong修饰或者重写set方法
// 知识点:集合类对象和非集合类对象的copy与mutableCopy
// [NSArray copy] // 浅复制(新的和原来的是一个array)
// [NSArray mutableCopy] // 深复制(array是新的,但是内容还是原来的,内容的指针没有变化)
// [NSMutableArray copy] // 深复制(array是新的,但是内容还是原来的,内容的指针没有变化)
// [NSMutableArray mutableCopy] // 深复制(array是新的,但是内容还是原来的,内容的指针没有变化)
}
/**
场景三:低版本系统使用高版本API
*/
- (void)case3 {if (@available(iOS 10.0, *)) {
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
}];
} else {
// Fallback on earlier versions
}
}
@end
1.2、原因
找不到方法iOS系统抛出异常崩溃
1.3、解决方案:
1、给NSObject添加一个分类,实现消息转发的几个方法
#import "NSObject+SelectorCrash.h"@implementation NSObject (SelectorCrash)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {if ([self respondsToSelector:aSelector]) {
// 已实现不做处理return [self methodSignatureForSelector:aSelector];
}return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"在 %@ 类中, 调用了没有实现的实例方法: %@ ",NSStringFromClass([self class]),NSStringFromSelector(anInvocation.selector));
}
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {if ([self respondsToSelector:aSelector]) {
// 已实现不做处理return [self methodSignatureForSelector:aSelector];
}return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(@"在 %@ 类中, 调用了没有实现的类方法: %@ ",NSStringFromClass([self class]),NSStringFromSelector(anInvocation.selector));
}
2、尽量避免使用performSelector一系列方法
3、delegate
方法调用前进行 respondsToSelector
判断,或者Release模式下使用ProtocolKit给协议添加默认实现防止崩溃,Debug模式下关闭默认实现
4、属性和成员变量不要重名定义,合理使用 synthesize 生成属性的 setter 和 getter 方法
5、在MRC模式下,变量的 retain 和 release 要谨慎,建议采用安全 release 方法,即 release 的对象置为 nil
6、在.h中声明的方法如果用不到就去掉,用得到就同时在.m文件中实现
7、可变属性(如NSMutableArray),不要使用copy修饰,或者重写set方法
8、使用高版本的系统方法的时候做判断
1.4、知识归纳:参考runtime 消息转发
消息转发机制主要包含三个步骤:
1、动态方法解析阶段
+(BOOL)resolveClassMethod:(SEL)sel或者
+(BOOL)resolveInstanceMethod:(SEL)sel
2、备用接收者阶段
- (id)forwardingTargetForSelector:(SEL)aSelector
3、完整消息转发阶段
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
和
- (void)forwardInvocation:(NSInvocation *)anInvocation
2、KVC造成的crash
2.1、场景对应的Code
#import "KvcCrashVC.h"@interface KvcCrashVCObj : NSObject
@property (nonatomic, strong) NSString *name;
@end
@implementation KvcCrashVCObj
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
}
- (id)valueForUndefinedKey:(NSString *)key {return nil;
}
@end
@interface KvcCrashVC ()
@end
@implementation KvcCrashVC
- (void)viewDidLoad {
[super viewDidLoad];
[self case1];
}
/**
场景一:对象不支持KVC
*/
- (void)case1 {
// reason: '[setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key key.'NSObject* obj = [[NSObject alloc]init];
[obj setValue:@"value" forKey:@"key"];
}
/**
场景二:key为nil
*/
- (void)case2 {
// reason: '*** -[KvcCrashVCObj setValue:forKey:]: attempt to set a value for a nil key'KvcCrashVCObj* obj = [[KvcCrashVCObj alloc]init];
// value 为nil不会崩溃
[obj setValue:nil forKey:@"name"];
// key为nil会崩溃(直接写nil编译器会提示警告,更多时候我们传的是变量)
[obj setValue:@"value" forKey:nil];
}
/**
场景三:key不是object的属性产生的crash
*/
- (void)case3 {
// reason: '[setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key falseKey.'KvcCrashVCObj* obj = [[KvcCrashVCObj alloc]init];
[obj setValue:nil forKey:@"falseKey"];
}
@end
2.2、原因
给不存在的key(包括key为nil)设置value
[obj setValue:@"value" forKey:@"UndefinedKey"];
[obj valueForKey:@"UndefinedKey"];
2.3、场景:
2.4、解决方案:
1、如果属性存在,利用iOS的反射机制来规避,NSStringFromSelector(@selector())将SEL反射为字符串作为key。这样在@selector()中传入方法名的过程中,编译器会有合法性检查,如果方法不存在或未实现会报黄色警告。
2、重写类的setValue:forUndefinedKey:和valueForUndefinedKey:
-(void)setValue:(id)value forUndefinedKey:(NSString *)key{
}
-(id)valueForUndefinedKey:(NSString *)key{return nil;
}
3、EXC_BAD_ACCESS
经过ARC的洗礼之后,普通的访问释放对象产生的EXC_BAD_ACCESS已经大量减少了,现在出现的EXC_BAD_ACCESS有很大一部分来自malloc的对象或者越界访问。
#import "BadAccessCrashVC.h"#import@interface BadAccessCrashVC (AssociatedObject)
@property (nonatomic, strong) UIView *associateView;
@end
@implementation BadAccessCrashVC (AssociatedObject)
- (void)setAssociateView:(UIView *)associateView {
objc_setAssociatedObject(self, @selector(associateView), associateView, OBJC_ASSOCIATION_ASSIGN);
//objc_setAssociatedObject(self, @selector(associateView), associateView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIView *)associateView {return objc_getAssociatedObject(self, _cmd);;
}
@end
@interface BadAccessCrashVC ()
@property (nonatomic, copy) void(^blcok)(void);
@property (nonatomic, weak) UIView* weakView;
@property (nonatomic, unsafe_unretained) UIView* unSafeView;
@property (nonatomic, assign) UIView* assignView;
@end
@implementation BadAccessCrashVC
- (void)viewDidLoad {
[super viewDidLoad];
[self case1];
}
/**
悬挂指针:访问没有实现的blcok
*/
- (void)case1 {
self.blcok();
}
/**
悬挂指针:对象没有被初始化
*/
- (void)case2 {
UIView* view = [UIView alloc];
view.backgroundColor = [UIColor blackColor];
[self.view addSubview:view];
}
/**
悬挂指针:访问的对象已经被释放掉
*/
- (void)case3 {
{
UIView* view = [[UIView alloc]init];
view.backgroundColor = [UIColor blackColor];
self.weakView = view;
self.unSafeView = view;
self.assignView = view;
self.associateView = view;
}
// ARC下weak对象释放后会自动置nil,因此下面的代码不会崩溃
[self.view addSubview:self.weakView];
// 野指针场景一:unsafe_unretained修饰的对象释放后,不会自动置nil,变成野指针,因此下面的代码会崩溃
[self.view addSubview:self.unSafeView];
// 野指针场景二:应该使用strong/weak修饰的对象,却错误的使用assign修饰,释放后不会自动置nil
[self.view addSubview:self.assignView];
// 野指针场景三:给类添加添加关联变量的时候,类似场景二,应该使用OBJC_ASSOCIATION_RETAIN_NONATOMIC修饰,却错误使用OBJC_ASSOCIATION_ASSIGN
[self.view addSubview:self.associateView];
}
@end
3.2、原因
出现悬挂指针,对象没有被初始化,或者访问的对象被释放
3.3、解决方案:
1、Debug阶段开启僵尸模式,Release时关闭僵尸模式
2、使用Xcode的Address Sanitizer检查地址访问越界
3、创建对象的时候记得初始化
4、对象的属性使用正确的修饰方式(strong/weak)
5、调用block的时候,做判断
4、KVO引起的崩溃
4.1、场景对应的Code
#import "KvoCrashVC.h"@interface KvoCrashVCObj : NSObject
@property (nonatomic, strong) NSString *name;
@end
@implementation KvoCrashVCObj
@end
@interface KvoCrashVC ()
@property (nonatomic, strong) KvoCrashVCObj *sObj;
@end
@implementation KvoCrashVC
- (void)viewDidLoad {
[super viewDidLoad];
self.sObj = [[KvoCrashVCObj alloc] init];
//#import// static dispatch_once_t onceToken;
// dispatch_once(&onceToken, ^{
// [XXShieldSDK registerStabilityWithAbility:(EXXShieldTypeKVO)];
// });
}
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent *)event {
[self func4];
}
/**
观察者是局部变量,会崩溃
*/
- (void)func1 {
// 崩溃日志:An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
KvoCrashVCObj* obj = [[KvoCrashVCObj alloc] init];
[self addObserver:obj forKeyPath:@"view" options:NSKeyValueObservingOptionNew
context:nil];
self.view = [[UIView alloc] init];
}
/**
被观察者是局部变量,会崩溃
*/
- (void)func2 {
// 崩溃日志:An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
KvoCrashVCObj* obj = [[KvoCrashVCObj alloc] init];
[obj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew
context:nil];
obj.name = @"";
}
/**
没有实现observeValueForKeyPath:ofObject:changecontext:方法:,会崩溃
*/
- (void)func3 {
// 崩溃日志:An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
[self.sObj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
self.sObj.name = @"0";
}
/**
重复移除观察者,会崩溃
*/
- (void)func4 {
// 崩溃日志:because it is not registered as an observer
[self.sObj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
self.sObj.name = @"0";
[self.sObj removeObserver:self forKeyPath:@"name"];
[self.sObj removeObserver:self forKeyPath:@"name"];
}
/**
重复添加观察者,不会崩溃,但是添加多少次,一次改变就会被观察多少次
*/
- (void)func5 {
[self.sObj addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL];
self.sObj.name = @"0";
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void *)context {
NSLog(@"keyPath = %@", keyPath);
}
// 总结:KVO有两种崩溃
// 1、because it is not registered as an observer
// 2、An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
@end
4.2、原因
添加了观察者,没有在正确的时机移除
4.3、解决方案:
1、addObserver和removeObserver一定要成对出现,
2、推荐使用FaceBook开源的第三方库 FBKVOController
5、集合类相关崩溃
5.1、场景对应的Code
#import "CollectionCrashVC.h"@interface CollectionCrashVC ()
@end
@implementation CollectionCrashVC
- (void)viewDidLoad {
[super viewDidLoad];
[self case4];
}
/**
场景一:数组越界
*/
- (void)case1 {
// reason: '*** -[__NSArrayI objectAtIndex:]: index 4 beyond bounds [0 .. 2]'NSArray* array = [[NSArray alloc]initWithObjects:@1, @2, @3, nil];
NSNumber* number = [array objectAtIndex:4];
NSLog(@"number = %@", number);
}
/**
场景二:向数组中添加nil元素
*/
- (void)case2 {
// reason: '*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil'NSMutableArray* array = [[NSMutableArray alloc]initWithObjects:@1, @2, @3, nil];
[array addObject:nil];
}
/**
场景三:数组遍历的时候使用错误的方式移除元素
*/
- (void)case3 {
NSMutableArray* array = [NSMutableArray array];
[array addObject:@1];
[array addObject:@2];
[array addObject:@3];
[array addObject:@4];
// 不崩溃
[array enumerateObjectsUsingBlock:^(NSNumber * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {if (obj.integerValue == 1) {
[array removeObject:obj];
}
}];
// 崩溃,reason: '*** Collectionwas mutated while being enumerated.'for (NSNumber* obj in array) {if (obj.integerValue == 2) {
[array removeObject:obj];
}
}
// dispatch_async(dispatch_get_global_queue(0, 0), ^{
// self.view.backgroundColor = [UIColor blueColor];
// });
}
/**
场景四:使用setObject:forKey:向字典中添加value为nil的键值对,推荐使用KVC的setValue:nil forKey:
*/
- (void)case4 {
NSMutableDictionary* dictionary = [NSMutableDictionary dictionary];
[dictionary setObject:@1 forKey:@1];
// 不崩溃:value为nil,只会移除key对应的键值对
[dictionary setValue:nil forKey:@1];
// 崩溃:reason: '*** -[__NSDictionaryM setObject:forKey:]: object cannot be nil (key: 1)'[dictionary setObject:nil forKey:@1];
}
@end
5.2、原因
越界、添加nil、多线程非原子性操作、遍历的同时移除元素
5.3、场景:
1、数组越界,访问下标大于数组的个数
2、向数组中添加空数据
3、多线程环境中,一个线程在读取,一个线程在移除
4、一边遍历数组,一边移除数组中的元素
5、多线程中操作可变数组(数组的扩容、访问僵尸对象)
5.4、解决方案:
1、给集合类添加category重写原来的方法,在内部做判断
2、使用Runtime把原来的方法替换成自定义的安全方法
3、给NSMutableDictionary添加元素的时候,使用setObject:forKey:向字典中添加value为nil的键值对,推荐使用KVC的setValue:nil forKey:。[mutableDictionary setValue:nil ForKey:@"name"]不会崩溃,只是从字典中移除name键值对。
4、因为NSMutableArray、NSMutableDictionary不是线程安全的,所以在多线程环境下要保证读写操作的原子性,使用 加锁 、信号量 、GCD串行队列 、GCD栅栏dispatch_barrier_async、CGD组的dispatch_group_enter和dispatch_group_leave
6、多线程中的崩溃
6.1、场景对应的Code
#import "ThreadCrashVC.h"@interface ThreadCrashVC ()
@property (nonatomic, strong) NSMutableArray *array;
@end
@implementation ThreadCrashVC
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent *)event {
[self case1];
}
/**
dispatch_group_leave比dispatch_group_enter执行的次数多
*/
- (void)case1 {
// 崩溃:Thread 1: EXC_BREAKPOINT (code=1, subcode=0x1054f6348)
dispatch_group_t group = dispatch_group_create();
dispatch_group_leave(group);
}
/**
在子线程更新UI
*/
- (void)case2 {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.view.backgroundColor = [UIColor redColor];
});
}
/**
多个线程同时释放一个对象
*/
- (void)case3 {
// ==================使用信号量同步后不崩溃==================
{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
__block NSObject *obj = [NSObject new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{while (YES) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
obj = [NSObject new];
dispatch_semaphore_signal(semaphore);
}
});while (YES) {
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
obj = [NSObject new];
dispatch_semaphore_signal(semaphore);
}
}
// ==================未同步则崩溃==================
{
__block NSObject *obj = [NSObject new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{while (YES) {
obj = [NSObject new];
}
});while (YES) {
obj = [NSObject new];
}
}
}
/**
多线程中的数组扩容、浅复制
扩容:数组的地址已经改变,报错was mutated while being enumerated
浅复制:访问僵尸对象,报错EXC_BAD_ACCESS
// 知识点:集合类对象和非集合类对象的copy与mutableCopy
// [NSArray copy] // 浅复制
// [NSArray mutableCopy] // 深复制
// [NSMutableArray copy] // 深复制
// [NSMutableArray mutableCopy] // 深复制
参考:
[Swift数组扩容原理](https://bestswifter.com/swiftarrayappend/)
[戴仓薯](https://juejin.im/post/5a9aa633518825556a71d9f3)
*/
-(void)case4 {
{
NSArray *array = @[@[@"a", @"b"], @[@"c", @"d"]];
NSArray *copyArray = [array copy];
NSMutableArray *mCopyArray = [array mutableCopy];
NSLog(@"array = %p,copyArray = %p,mCopyArray = %p", array, copyArray, mCopyArray);
}
{
NSMutableArray *array = [NSMutableArray arrayWithObjects:[NSMutableString stringWithString:@"a"],@"b",@"c",nil];
NSArray *copyArray = [array copy];
NSMutableArray *mCopyArray = [array mutableCopy];
NSLog(@"array = %p,copyArray = %p,mCopyArray = %p", array, copyArray, mCopyArray);
}
dispatch_queue_t queue1 = dispatch_queue_create("queue1", 0);
dispatch_queue_t queue2 = dispatch_queue_create("queue2", 0);
NSMutableArray* array = [NSMutableArray array];
dispatch_async(queue1, ^{while (true) {if (array.count < 10) {
[array addObject:@(array.count)];
} else {
[array removeAllObjects];
}
}
});
dispatch_async(queue2, ^{while (true) {
// case 1:数组扩容for (NSNumber* number in array) {
NSLog(@"%@", number);
}
// case 2:数组扩容
NSArray* immutableArray = array;for (NSNumber* number in immutableArray) {
NSLog(@"%@", number);
}
// case 3:浅复制 在 [NSArray copy] 的过程,
// copy 方法内部调用initWithArray:range:copyItems: 时
// 数组被另一个线程清空,range 不一致导致抛出 exception
NSArray* immutableArray1 = [array copy];for (NSNumber* number in immutableArray1) {
NSLog(@"%@", number);
}
// case 4:浅复制 数组内的对象被其他线程释放,访问僵尸对象
NSArray* immutableArray2 = [array mutableCopy];for (NSNumber* number in immutableArray2) {
NSLog(@"%@", number);
}
}
});
}
@end
6.2、原因
死锁、子线程中更新UI、多个线程同时释放一个对象
6.3、场景
1、在子线程中更新UI
2、dispatch_group crash,dispatch_group_leave
的次数比dispatch_group_enter
次数多。参考:iOS疑难问题排查之深入探究dispatch_group crash
3、多线程下非线程安全类的使用,如NSMutableArray
、NSMutableDictionary
。NSCache是线程安全的。
4、数据缓存到磁盘和读取。
6.4、解决方案:
多线程遇到需要同步的时候,加锁,添加信号量等进行同步操作。一般多线程发生的Crash,会收到SIGSEGV信号,表明试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据。
7、Socket长连接,进入后台没有关闭
当服务器close一个连接时,若client端接着发数据。根据TCP协议的规定,会收到一个RST响应,client再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不要再写了。而根据信号的默认处理规则,SIGPIPE信号的默认执行动作是terminate(终止、退出),所以client会退出。
长连接socket或重定向管道进入后台,没有关闭导致崩溃的解决办法:
7.1、解决方案:
方法一:1、切换到后台是,关闭长连接和管道,回到前台重新创建。
方法二:2、使用signal(SIGPIPE,SIG_IGN),将SIGPIP交给系统处理,这么做将SIGPIPE设为SIG_IGN,使客户端不执行默认操作,即不退出。
8、Watch Dog超时造成的crash
主线程执行耗时操作,导致主线程被卡超过一定时间。一般异常编码是0x8badf00d,表示应用发生watch dog超时而被iOS终止,通常是应用花费太多的时间无法启动、终止或者响应系统事件。
8.1、解决方案:
主线程只负责更新UI和事件响应,将耗时操作(网络请求、数据库读写等)异步放到后台线程执行。
9、后台返回NSNull导致的崩溃,多见于Java做后台服务器开发语言
9.1、场景对应的Code
NULL
:用于普通类型,例如NSInteger
nil
:用于OC对象(除了类这个对象),给nil对象发送消息不会crashNil
:用于Class类型对象的赋值(类是元类的实例,也是对象)NSNull
:用于OC对象的站位,一般会作为集合中的占位元素,给NSNull对象发送消息会crash的,后台给我们返回的就是NSNull对象
9.2、解决方法
利用消息转发。参考:NullSafe。当我们给一个NSNull对象发送消息的话,可能会崩溃(null是有内存的),而发送给nil的话,是不会崩溃的。
10、在iOS中捕获异常信息
崩溃主要是由于 Mach
异常、Objective-C
异常(NSException
)引起的,同时对于 Mach
异常,到了 BSD
层会转换为对应的 Signal
信号,那么我们也可以通过捕获信号,来捕获 Crash
事件。针对 NSException
可以通过注册 NSUncaughtExceptionHandler
捕获异常信息。