iOS – 常见Crash

常见的Crash

  1. 找不到方法的实现unrecognized selector sent to instance
  2. KVC造成的Crash
  3. EXC_BAD_ACCESS
  4. KVO引起的崩溃
  5. 集合类相关崩溃
  6. 多线程中崩溃
  7. Socket长连接,进入后台没有关闭
  8. Watch Dog造成的崩溃
  9. 后台返回NSNull造成的崩溃

1 找不到方法的实现unrecognized selector sent to instance

1.1 对应代码

person.h

@protocol PersonDelegate <NSObject>

- (void)didChangedName;

@end

@interface Person : NSObject

//该方法在.m未实现
- (void)eatFood;

@property (nonatomic, weak) id<PersonDelegate>delegate;

@end
复制代码

ViewController.m

#import "ViewController.h"
#import "Person.h"

@interface ViewController ()

@property (nonatomic, strong) NSMutableArray *mutableArray1;
@property (nonatomic, copy) NSMutableArray *mutableArray2;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self case4];
}

- (void)case1 {
    /*
     crash1. eatFood 声明未实现
     解决方案:.m实现eatFood方法,或消息转发
     */
    Person *person = [[Person alloc] init];
    person.delegate = self;
    [person eatFood];
}

- (void)case2 {
    /*
     crash2. 代理未实现
     解决方案:
     if (person.delegate && [person.delegate respondsToSelector:@selector(didChangedName)]) {
         [person.delegate didChangedName];
     }
     */
    Person *person = [[Person alloc] init];
    person.delegate = self;
    [person.delegate didChangedName];
}

- (void)case3 {
    NSMutableArray *array = [NSMutableArray arrayWithObjects:@"1",@"2",@"3", nil];
    self.mutableArray1 = array; //strong修饰的 mutableArray1,此处是NSMutableArray,与array指针地址一致
    self.mutableArray2 = array; //copy修饰的mutableArray2,此处是NSArray,是新对象,与array指针地址不一致
    [self.mutableArray1 addObject:@"4"];
    //下边这行代码导致: -[__NSArrayI addObject:]: unrecognized selector sent to instance 0x600002628ea0
    //原因:copy修饰的NSMutableArray,在51行会将array浅拷贝,self.mutableArray2变成NSArray, [NSArray addObject] crash
    //解决方案:用strong修饰或重写setter方法
    [self.mutableArray2 addObject:@"4"];
    
    //附:
    //[NSArray copy] 浅拷贝,生成NSArray
    //[NSArray mutableCopy] 深拷贝, 生成NSMutableArray
    //[NSMutableArray copy] 深拷贝,生成是NSArray
    //[NSMutableArray mutableCopy] 深拷贝,生成NSMutableArray
}

- (void)case4 {
    //低版本调用高版本API
    if (@available(iOS 10.0, *)) {
        [NSTimer scheduledTimerWithTimeInterval:1 repeats:true block:^(NSTimer * _Nonnull timer) {
        }];
    } else {
        // Fallback on earlier versions
    }
}
复制代码

1.3 原因

找不到执行的方法(附:给nil对象发消息不会crash,但给非nil对象发未实现的消息会crash)

1.4 解决方案总结

  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(@"在 %@ 类中, 调用了没有实现的 %@ 实例方法", [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(@"在 %@ 类中, 调用了没有实现的 %@ 类方法", [self class], NSStringFromSelector(anInvocation.selector));
}

@end
复制代码
  1. delegate 方法调用前进行delegate,respondsToSelector判断
  2. .h声明的方法添加实现
  3. NSMutableArray尽量使用strong修饰(同时注意数据修改问题)
  4. 使用系统API时进行版本判断

1.5 Runtime消息动态解析、转发

消息动态解析

整个消息发生流程

2. KVC造成的crash

2.1 对应代码

@interface KVCCrashVCObj : NSObject

@property (nonatomic, copy) NSString *nickname;

@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.view.backgroundColor = [UIColor lightGrayColor];
    
    //[self case1];
    [self case2];
}

- (void)case1 {
    /*
     对象不支持kvc
     reason: '[<KVCCrashVCObj 0x6000008ccd30> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key key.'
        
     */
    KVCCrashVCObj *obj = [[KVCCrashVCObj alloc] init];
    [obj setValue:@"value" forKey:@"key"];
}

- (void)case2 {
    /*
     key设置为nil
     reason: '*** -[KVCCrashVCObj setValue:forKey:]: attempt to set a value for a nil key'
     */
    KVCCrashVCObj *obj = [[KVCCrashVCObj alloc] init];
    [obj setValue:nil forKey:@"nickname"];
    [obj setValue:@"value" forKey:nil];
}

- (void)case3 {
    /*
     通过不存在的key取值
     reason: '[<KVCCrashVCObj 0x6000019f3150> valueForUndefinedKey:]: this class is not key value coding-compliant for the key key.'
     解决方案:
     KVCCrashVCObj 重写 - (id)valueForUndefinedKey:(NSString *)key
     */
    KVCCrashVCObj *obj = [[KVCCrashVCObj alloc] init];
    NSString *nickname = [obj valueForKey:@"key"];
}

@end

复制代码

2.2 原因

给不存在的key或nil设置value值, 通过不存在的key取值

[obj setValue: @"value" forKey: @"undefinedKey"];
[obj setValue: @"value" forKey: nil];
[obj valueForKey: @"undifinedKey"];

复制代码

2.3 解决方案

  1. 如果属性存在,通过iOS的反射机制来规避,[obj setValue:@"value" forKey:NSStringFromSelector(@selector(undifinedKey))];, 将SEL反射为字符串做为key,这样在@selecor()中传入方法名的时候,编译器会做检查,如果方法不存在会报警告
  2. 重写类的setValue:forUndefinedKey:valueForUndefinedKey:

3. KVO导致的crash

3.1 KVO知识回顾

3.1.1 KVO参数说明

    KVOCrashObj* obj = [[KVOCrashObj alloc] init];
    /*
     1. 观察者:obj
     2. 被观察者:self
     3. 观察的对象:view
     4. context:可选的参数,会随着观察消息传递,
     用于区分接收该消息的观察者。一般情况下,只需通过 keyPath 就可以判断接收消息的观察者。但是当父类子类都观察了同一个 keyPath 时,仅靠 keyPath 就无法判断消息该传给子类,还是传给父类。
     5. 需要在观察者类KVOCrashObj里实现 observeValueForKeyPath: ofObject: change: context:,才能接受到被观察者self的view变化
     
     */
    [self addObserver:obj forKeyPath:@"view" options:NSKeyValueObservingOptionNew context:nil];
复制代码

3.1.2 KVO本质

iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)

答. 当一个对象使用了KVO监听,iOS系统会修改这个对象的isa指针,改为指向一个全新的通过Runtime动态创建的子类,子类拥有自己的set方法实现,set方法实现内部会顺序调用willChangeValueForKey方法、原来的setter方法实现、didChangeValueForKey方法,而didChangeValueForKey方法内部又会调用监听器的observeValueForKeyPath:ofObject:change:context:监听方法。

3.1 对应代码

#import "KVOCrashVC.h"

@interface KVOCrashObj : NSObject
@property (nonatomic, copy) NSString *nickname;
@end

@implementation KVOCrashObj
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@ 观察到了%@ 的 %@发生了改变",[self class],[object class],keyPath);
}
@end

@interface KVOCrashVC ()
@property (nonatomic, strong) KVOCrashObj *kvoObj;
@end

@implementation KVOCrashVC

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor lightTextColor];
    
    self.kvoObj = [[KVOCrashObj alloc] init];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self case2];
}


- (void)case1 {
    /*
     self观察obj的nickname值改变,在self vc没有实现observeValueForKeyPath:ofObject:changecontext:导致crash
     reason: '<KVOCrashVC: 0x7f84dc617a20>: An -observeValueForKeyPath:ofObject:change:context: message was received but not handled.
     Key path: nickname
     Observed object: <KVOCrashObj: 0x60000268a120>
     Change: {
         kind = 1;
         new = "";
     }
     Context: 0x0'
     */
    
    KVOCrashObj* obj = [[KVOCrashObj alloc] init];
    [self addObserver:obj forKeyPath:@"view" options:NSKeyValueObservingOptionNew context:nil];
    [obj addObserver:self
          forKeyPath:@"nickname"
             options:NSKeyValueObservingOptionNew
             context:nil];
    obj.nickname = @"";
}

- (void)case2 {
    /* 重复移除观察者导致崩溃
     reason: 'Cannot remove an observer <KVOCrashVC 0x7f8199912f00> for the key path "nickname" from <KVOCrashObj 0x6000004f5780> because it is not registered as an observer.'
     */
    [self.kvoObj addObserver:self forKeyPath:@"nickname" options:NSKeyValueObservingOptionNew context:nil];
    self.kvoObj.nickname = @"objc.c";
    [self.kvoObj removeObserver:self forKeyPath:@"nickname"];
    [self.kvoObj removeObserver:self forKeyPath:@"nickname"];
}
    

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    NSLog(@"%@ 观察到了%@ 的 %@发生了改变",[self class],[object class],keyPath);
}

@end

复制代码

3.2 原因

  1. 添加观察者后未实现observeValueForKeyPath: ofObject: change: context:方法
  2. 重复移除观察者

3.3 解决方案

  1. 观察者必须实现observeValueForKeyPath: ofObject: change: context:方法
  2. addobserverremoveObserver必须成对出现

4. EXC_BAD_ACCESS

4.1 对应代码

#import "BadAccessCrashVC.h"
#import <objc/runtime.h>

@interface BadAccessCrashVC (AssociatedObject)
@property (nonatomic, strong) UIView *associatedView;
@end

@implementation BadAccessCrashVC (AssociatedObject)

- (void)setAssociatedView:(UIView *)associatedView {
    /*
     self: 关联对象的类
     key:  要保证全局唯一,key与关联的对象是一一对应关系,必须全局唯一,通常用@selector(methodName)做为key
     value: 要关联的对象
     policy:关联策略
     OBJC_ASSOCIATION_COPY: 相当于@property(atomic,copy)
     OBJC_ASSOCIATION_COPY_NONATOMIC: 相当于@property(nonatomic, copy)
     OBJC_ASSOCIATION_ASSIGN: 相当于@property(assign)
     OBJC_ASSOCIATION_RETAIN: 相当于@property(atomic, strong)
     OBJC_ASSOCIATION_RETAIN_NONATOMIC: 相当于@property(nonatomic, strong)
     */
    objc_setAssociatedObject(self, @selector(associatedView), associatedView, OBJC_ASSOCIATION_ASSIGN);
    //objc_setAssociatedObject(self, @selector(associatedView), associatedView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (UIView *)associatedView {
    return objc_getAssociatedObject(self, _cmd);
}
@end


@interface BadAccessCrashVC ()

@property (nonatomic, copy) void(^block)(void);
@property (nonatomic, weak) UIView *weakView;
@property (nonatomic, unsafe_unretained) UIView *unsafeView;
@property (nonatomic, assign) UIView *assignView;
@end

@implementation BadAccessCrashVC

- (void)viewDidLoad {
    self.view.backgroundColor = [UIColor orangeColor];
    [super viewDidLoad];
    
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self case3];
}

- (void)case1 {
    /*
     悬挂指针:访问没有实现的Block
     Thread 1: EXC_BAD_ACCESS (code=1, address=0x10)
     */
    self.block();
}

- (void)case2 {
    /*
     悬挂指针:对象没有被初始化
     Thread 1: EXC_BAD_ACCESS (code=1, address=0x0)
     */
    UIView *view = [UIView alloc];
    view.backgroundColor = [UIColor redColor];
    [self.view addSubview:view];
}

- (void)case3 {
    {
    UIView *view = [[UIView alloc]init];
    view.backgroundColor = [UIColor redColor];
    self.weakView = view;
    self.unsafeView = view;
    self.assignView = view;
    self.associatedView = view;
    }
    
    //addSubview:nil时不会crash
    //以下崩溃都是Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
    //不会crash, arc下view释放后,weakView会置为nil,因此这行代码不会崩溃
    [self.view addSubview:self.weakView];
    //野指针场景一: unsafeunreatin修饰的对象释放后,不会自动置为nil,变成野指针,因此崩溃
    [self.view addSubview:self.unsafeView];
    //野指针场景二:应该使用strong/weak修饰的对象,却错误的使用了assign,释放后不会置为nil
    [self.view addSubview:self.assignView];
    //野指针场景三:给类添加关联变量时,类似场景二,应该使用OBJC_ASSOCIATION_RETAIN,却错误的使用了OBJC_ASSOCIATION_ASSIGN
    [self.view addSubview:self.associatedView];
}

@end

复制代码

4.2 原因

出现悬挂指针、访问未被初始化对象、访问野指针

4.3 解决方案

  1. Debug环境开启Zombie Objects,Release关闭
  2. 使用Xcode的Address Sanitizer检查地址访问越界
  3. 创建对象的时候记得初始化
  4. 对象的修饰符使用正确
  5. 调用Block的时候,做判断

5 集合类崩溃

5.1 对应代码

#import "CollectionCrashVC.h"

@interface CollectionCrashVC ()

@end

@implementation CollectionCrashVC

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor yellowColor];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self case3];
}

- (void)case1 {
    /*
    数组越界
     reason: '*** -[__NSArrayM objectAtIndex:]: index 3 beyond bounds [0 .. 2]
     */
    NSMutableArray *array = [NSMutableArray arrayWithObjects:@1, @2, @3, nil];
    NSNumber *number = [array objectAtIndex:3];
    NSLog(@"number: %@", number);
}

- (void)case2 {
    /*
     向集合中插入nil元素
     reason: '*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil
     */
    NSMutableArray *array = [NSMutableArray arrayWithObjects:@1, @2, @3, nil];
    [array addObject:nil];
}

- (void)case3 {
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    [dict setObject:@1 forKey:@"1"];
    //不会崩溃,value为nil,只会移除对应的键值对
    [dict setValue:nil forKey:@"1"];
    //崩溃:reason: '*** -[__NSDictionaryM setObject:forKey:]: object cannot be nil (key: 1)'
    [dict setObject:nil forKey:@"1"];
}

@end
复制代码

5.2 原因

  1. 数组越界
  2. 向数组中添加nil元素
  3. 一边遍历数组,一边移除数组中元素
  4. 多线程中操作数组:数组扩容、访问僵尸对象等
  5. 字典setObject:nil forKey:@"key"

5.4 解决方案

  1. runtime swizzling交换集合取值方法,取值的时候做判断
  2. NSMutableArray添加元素时,使用setValue:forKey:,这个方法向字典中添加nil时,不会崩溃,只会删除对应键值对
  3. 因为NSMutableArray、NSMutableDictionary不是线程安全的,所以在多线程环境下要保证读写操作的原子性,可以加锁、信号量、GCD串行队列、GCD栅栏

dispatch_barrier_asyncdispatch_group

6. 多线程崩溃

6.1 对应代码

#import "ThreadCrashVC.h"

@interface ThreadCrashVC ()

@end

@implementation ThreadCrashVC

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor redColor];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self case4];
}

- (void)case1 {
    //dispatch_group_leave 比dispatch_group_enter多
    //Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_leave(group);
}

- (void)case2 {
    //子线程中刷新UI
    //ain Thread Checker: UI API called on a background thread: -[UIViewController view]
    dispatch_queue_t queue = dispatch_queue_create("com.objc.c", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue , ^{
        NSLog(@"thread: %@", [NSThread currentThread]);
        self.view.backgroundColor = [UIColor yellowColor];
    });
}

- (void)case3 {
    //使用信号量后不会崩溃
    {
        dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
        __block NSObject *obj = [[NSObject alloc] init];
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            while (YES) {
                NSLog(@"dispatch_async -- 1");
                dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
                obj = [[NSObject alloc] init];
                dispatch_semaphore_signal(semaphore);
            }
        });
        
        while (YES) {
            NSLog(@"dispatch_sync -- 2");
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            obj = [[NSObject alloc] init];
            dispatch_semaphore_signal(semaphore);
        }

    }
    
    //不使用信号量,多线程同时释放对象导致崩溃
    {
        __block NSObject *obj = [[NSObject alloc] init];
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            while (YES) {
                NSLog(@"dispatch_async -- 3");
                obj = [[NSObject alloc] init];
            }
        });
        
        while (YES) {
            NSLog(@"dispatch_sync -- 4");
            obj = [[NSObject alloc] init];
        }
    }
}

- (void)case4 {
    dispatch_queue_t queue1 = dispatch_queue_create("com.objc.c1", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue2 = dispatch_queue_create("com.objc.c2", DISPATCH_QUEUE_SERIAL);
    NSMutableArray *array = [NSMutableArray array];
    dispatch_async(queue1, ^{
        NSLog(@"queue1: %@", [NSThread currentThread]);
        while (YES) {
            if (array.count < 10) {
                [array addObject:@(array.count)];
            } else {
                [array removeAllObjects];
            }
        }
    });
    
    dispatch_async(queue2, ^{
        NSLog(@"queue2: %@", [NSThread currentThread]);
        while (YES) {
            /*
             数组地址已经改变
             reason: '*** Collection <__NSArrayM: 0x6000020319b0> was mutated while being enumerated.'
             */
            for (NSNumber *number in array) {
                NSLog(@"queue2 forin array %@", number);
            }
            
            /*
             reason: '*** Collection <__NSArrayM: 0x600002072d60> was mutated while being enumerated.'
             */
            NSArray *array2 = array;
            for (NSNumber *number in array2) {
                NSLog(@"queue2 forin array2 %@", number);
            }
            
            /*
             在[NSArray copy]的时候,copy方法内部调用`initWithArray:range:copyItem:`时
             NSArray被另一个线程清空,range不一致导致跑出异常
             reason: '*** -[__NSArrayM getObjects:range:]: range {0, 2} extends beyond bounds for empty array'
             复制过程中数组内对象被其它线程释放,导致访问僵尸对象
             Thread 4: EXC_BAD_ACCESS (code=1, address=0x754822c49fc0)
             */
            NSArray *array3 = [array copy];
            for (NSNumber *number in array3) {
                NSLog(@"queue2 forin array3 %@", number);
            }
            
            /*
             复制过程中数组内对象被其它线程释放,导致访问僵尸对象
             Thread 12: EXC_BAD_ACCESS (code=EXC_I386_GPFLT)
             */
            NSArray *array4 = [array mutableCopy];
            for (NSNumber *number in array4) {
                NSLog(@"queue2 forin array4 %@", number);
            }
        }
    });
}

@end
复制代码

6.2 原因

  1. 子线程刷新UI
  2. dispatch_group_leave 比dispatch_group_enter多
  3. 多个线程同时访问、释放同一对象
  4. 多线程下访问数据:NSMutableArray、NSMutaleDictionary, NSCache是线程安全的

6.3 解决方案

多线程遇到数据同步时,需要加锁、信号量等进行同步操作,一般多线程发生的Crash,会收到SIGSEGV。表明试图访问未分配给自己的内存,或视图往没有读写权限的内存中写数据

7 Wathc Dog造成的Crash

主线程耗时操作,造成主线程被卡超过一定时长,App被系统终止,一般异常编码是0x8badf00d,通常是引用花太长时间无法启动、终止或响应系统事件

7.1 解决方案

主线程做UI刷新和事件响应,将耗时操作(网络请求、数据库读取)放到异步线程

8 后台返回NSNull导致崩溃,多见于JAVA后台返回

8.1 对应代码

#import "NSNullCrashVC.h"

@interface NSNullCrashVC ()

@end

@implementation NSNullCrashVC

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor blueColor];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self case1];
}

- (void)case1 {
    /*
     reason: '-[NSNull integerValue]: unrecognized selector sent to instance 0x7fff8004b700'
     */
    NSNull *null = [[NSNull alloc] init];
    NSMutableDictionary *dict = [NSMutableDictionary dictionary];
    [dict setValue:null forKey:@"key"];
    NSInteger integer = [[dict valueForKey:@"key"] integerValue];
}

@end
复制代码
  • NULL:
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享