这篇文章列出了几种常见的 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 跟踪记录下来
- 代码写出来应该是看起来很清晰的,如果看起来很绕,那么是需要重构了
如果您觉得本文对您有所帮助,请点击「喜欢」来支持我。
转载请注明出处,有任何疑问都可联系我,欢迎探讨。
网友评论