这篇文章列出了9种常见的crash,原文写得很好,我这里对照我自己遇到过的情况再整理记录下。

(一)KVO

KVO的一种常用场景是view对象监听view model对象实现实时刷新UI,例如有一个table view,每个cell都监听对应的cell model,这样数据源数组中只有一个对象的属性发生改变时就不需要reload整个列表。

使用KVO有一个常见的crash就是没有移除监听,我们需要在dealloc方法中执行removeObserver方法。这里推荐facebook开源的KVOController,让我们更方便地使用KVO。

(二)遍历可变集合时对集合做修改

我们经常会遇到集合遍历的crash,有一点需要注意,在遍历可变集合(NSMutableArray,NSMutableDictionary,NSMutableSet)时,不能够对集合做修改,例如增加或删除集合中的元素。这个问题最好是从代码规范上避免,例如接口中不应该暴露可变集合,而是暴露readonly的集合。以下是推荐的一种写法:

People.h

#import <Foundation/Foundation.h>

@interface People : NSObject

@property (nonatomic, strong, readonly) NSArray *friends;

- (void)addFriend:(id)aFriend;
- (void)removeFriend:(id)aFriend;

@end复制代码

People.m

#import "People.h"

@interface People ()

@property (nonatomic, strong) NSMutableArray *internalFriends;

@end

@implementation People

- (void)dealloc
{
    //
}

- (instancetype)init
{
    self = [super init];
    if (self) {
        _internalFriends = [NSMutableArray new];
    }
    return self;
}

- (void)addFriend:(id)aFriend
{
    if (aFriend == nil) {
        return;
    }
    @synchronized(self)
    {
        [_internalFriends addObject:aFriend];
    }
}

- (void)removeFriend:(id)aFriend
{
    if (aFriend == nil) {
        return;
    }
    @synchronized(self)
    {
        [_internalFriends removeObject:aFriend];
    }
}

//NSMutableArray copy -> NSArray
- (NSArray *)friends
{
    return [_internalFriends copy];
}

@end复制代码

还有一点要注意的是,对于第三方接口返回的集合,我们都要怀疑其正确性,有可能接口中写明是不可变的但是实际返回的是可变集合,如果我们直接按照不可变来使用就有可能触发crash,因此在集合遍历前先对第三方接口返回的数据做一次copy操作是一个好的习惯。

(三)NSNotification

NSNotification是一种一对多的监听机制,有一种常见的crash是对象dealloc后没有移除监听。

移除监听的方式

我们可以根据具体的通知名称移除,例如

[[NSNotificationCenter defaultCenter] removeObserver:self name:kSomeNotificationName object:someObject];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kSomeOtherNotificationName object:someOtherObject];
etc...复制代码

上述方法没有问题,但是不利于维护,比如后期又有需求需要添加新的通知来实现,对应的就需要添加代码来移除,要是一不小心忘记移除就会触发crash,更加推荐的方式是在dealloc中使用
[[NSNotificationCenter defaultCenter] removeObserver:self];来移除

重复监听

在注册监听通知时有一个问题需要注意,经测试,重复注册会导致回调方法进入多次,注册几次,回调就会进入几次。我们经常在viewDidLoad中注册监听,但是view是有可能unloaded再reloaded的,因此viewDidLoad就有可能执行多次导致重复注册。

在init方法中注册,在dealloc方法中移除

对于一个对象,它的init方法只会执行一次,dealloc方法也是,因此在这两个方法中执行注册和移除就能保证注册和移除是平衡的,降低了问题排查的难度。

避免使用addObserverForName

[NSNotificationCenter addObserverForName:object:queue:usingBlock:] 提供了block的方法来使用通知,但是我们应该避免使用这种方式,因为这需要我们在后续代码里单独移除,这就增加了出错的可能,不像上述提到的能在dealloc统一移除。

(四)处理空的情况

我们知道,在Objective-C中,对nil发送消息是没有问题的,例如

[thing doStuff];

这种写法没有问题,但是如果参数是nil,则取决于具体的方法是如何实现的,例如:

[self doStuff:thing];

这种情况就要看thing是拿来做什么,如果方法实现里有如下代码

menuItem.title = thing;

menuItem是NSMenuItem,那么当thing为空时就会导致crash。

一种推荐的做法是使用断言对参数做空的判断,具体如下:

- (void)someMethod:(id)someParameter {
  NSParameterAssert(someParameter);
  …do whatever…
}复制代码

(五)越界

常见的越界crash就是数组越界,当然还有其他的越界,比如NSrange,对于这些的使用,推荐的做法是在使用前都做一下范围校验,这也是需要注意的点。

(六)非主线程处理UI事件

在非主线程处理UI事件会导致不可预知的事情发生,有可能crash,有可能是UI显示异常。比如我们在子线程执行了一段耗时的计算任务,然后将计算结果传递给UI去更新显示,这时候我们需要

dispatch_async(dispatch_get_main_queue(), ^{

    });复制代码

另外,原文作者还提出了一些他的编程实践经验,例如:

  • 应尽可能的将任务放到主线程排队执行,这样能避免大多数多线程问题,除非是经检测有性能瓶颈的任务需要放到子线程,并且他也是偏向于将独立的任务放到子线程中
  • 尽可能使用点语法(_property = xxx的方式赋值不会触发KVO)、ARC、weak属性
  • 建立完善的crash收集机制,并且将bug跟踪记录下来
  • 代码写出来应该是看起来很清晰的,如果看起来很绕,那么是需要重构了