这篇文章列出了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跟踪记录下来
- 代码写出来应该是看起来很清晰的,如果看起来很绕,那么是需要重构了