iOS 开发之避免 crash
Jonkamkw
8年前
<p>这篇文章 列出了几种常见的 crash,原文写得很好,我这里对照我自己遇到过的情况再整理记录下。</p> <h3><strong>(一)KVO</strong></h3> <p>KVO 的一种常用场景是 view 对象监听 view model 对象实现实时刷新 UI,例如有一个 table view,每个 cell 都监听对应的 cell model,这样数据源数组中只有一个对象的属性发生改变时就不需要 reload 整个列表。</p> <p>使用 KVO 有一个常见的 crash 就是没有移除监听,我们需要在 dealloc 方法中执行 removeObserver 方法。这里推荐 非死book 开源的 KVOController ,让我们更方便地使用 KVO。</p> <h3><strong>(二)遍历可变集合时对集合做修改</strong></h3> <p>我们经常会遇到集合遍历的 crash,有一点需要注意,在遍历可变集合(NSMutableArray,NSMutableDictionary,NSMutableSet)时,不能够对集合做修改,例如增加或删除集合中的元素。这个问题最好是从代码规范上避免,例如接口中不应该暴露可变集合,而是暴露 readonly 的集合。以下是推荐的一种写法:</p> <p>People.h</p> <pre> <code class="language-objectivec">#import <Foundation/Foundation.h> @interface People : NSObject @property (nonatomic, strong, readonly) NSArray *friends; - (void)addFriend:(id)aFriend; - (void)removeFriend:(id)aFriend; @end</code></pre> <p>People.m</p> <pre> <code class="language-objectivec">#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</code></pre> <p>还有一点要注意的是,对于第三方接口返回的集合,我们都要怀疑其正确性,有可能接口中写明是不可变的但是实际返回的是可变集合,如果我们直接按照不可变来使用就有可能触发 crash,因此在集合遍历前先对第三方接口返回的数据做一次 copy 操作是一个好的习惯。</p> <h3><strong>(三)NSNotification</strong></h3> <p>NSNotification 是一种一对多的监听机制,有一种常见的 crash 是对象 dealloc 后没有移除监听。</p> <p>移除监听的方式</p> <p>我们可以根据具体的通知名称移除,例如</p> <pre> <code class="language-objectivec">[[NSNotificationCenter defaultCenter] removeObserver:self name:kSomeNotificationName object:someObject]; [[NSNotificationCenter defaultCenter] removeObserver:self name:kSomeOtherNotificationName object:someOtherObject]; etc...</code></pre> <p>上述方法没有问题,但是不利于维护,比如后期又有需求需要添加新的通知来实现,对应的就需要添加代码来移除,要是一不小心忘记移除就会触发 crash,更加推荐的方式是在 dealloc 中使用</p> <p>[[NSNotificationCenter defaultCenter] removeObserver:self]; 来移除</p> <p>重复监听</p> <p>在注册监听通知时有一个问题需要注意,经测试,重复注册会导致回调方法进入多次,注册几次,回调就会进入几次。我们经常在 viewDidLoad 中注册监听,但是view是有可能 unloaded 再 reloaded 的,因此 viewDidLoad 就有可能执行多次导致重复注册。</p> <p>在init方法中注册,在dealloc方法中移除</p> <p>对于一个对象,它的 init 方法只会执行一次,dealloc 方法也是,因此在这两个方法中执行注册和移除就能保证注册和移除是平衡的,降低了问题排查的难度。</p> <p>避免使用addObserverForName</p> <p>[NSNotificationCenter addObserverForName:object:queue:usingBlock:] 提供了 block 的方法来使用通知,但是我们应该避免使用这种方式,因为这需要我们在后续代码里单独移除,这就增加了出错的可能,不像上述提到的能在 dealloc 统一移除。</p> <h3><strong>(四)处理空的情况</strong></h3> <p>我们知道,在 Objective-C 中,对 nil 发送消息是没有问题的,例如</p> <p>[thing doStuff];</p> <p>这种写法没有问题,但是如果参数是 nil,则取决于具体的方法是如何实现的,例如:</p> <p>[self doStuff:thing];</p> <p>这种情况就要看 thing 是拿来做什么,如果方法实现里有如下代码</p> <p>menuItem.title = thing;</p> <p>menuItem 是 NSMenuItem,那么当 thing 为空时就会导致 crash。</p> <p>一种推荐的做法是使用断言对参数做空的判断,具体如下:</p> <pre> <code class="language-objectivec">- (void)someMethod:(id)someParameter { NSParameterAssert(someParameter); …do whatever… }</code></pre> <h3><strong>(五)越界</strong></h3> <p>常见的越界 crash 就是数组越界,当然还有其他的越界,比如 NSrange,对于这些的使用,推荐的做法是在使用前都做一下范围校验,这也是需要注意的点。</p> <h3><strong>(六)非主线程处理UI事件</strong></h3> <p>在非主线程处理UI事件会导致不可预知的事情发生,有可能 crash,有可能是 UI 显示异常。比如我们在子线程执行了一段耗时的计算任务,然后将计算结果传递给 UI 去更新显示,这时候我们需要</p> <pre> <code class="language-objectivec">dispatch_async(dispatch_get_main_queue(), ^{ });</code></pre> <p>另外, 原文 作者还提出了一些他的编程实践经验,例如:</p> <ul> <li>应尽可能的将任务放到主线程排队执行,这样能避免大多数多线程问题,除非是经检测有性能瓶颈的任务需要放到子线程,并且他也是偏向于将独立的任务放到子线程中</li> <li>尽可能使用点语法(_property = xxx的方式赋值不会触发KVO)、ARC、weak属性</li> <li>建立完善的 crash 收集机制,并且将 bug 跟踪记录下来</li> <li>代码写出来应该是看起来很清晰的,如果看起来很绕,那么是需要重构了</li> </ul> <p> </p> <p> </p> <p>来自:http://www.jianshu.com/p/dce3c87d3ca1</p> <p> </p>