原文地址
App crash是让大家很头疼的一件事情, 有些crash比较明显, 比如数组越界, 未实现的selector等等, 有些就比较隐蔽, 作者在写了一系列的文章, 描述了潜在的crash问题, 我针对自己的一些经验, 和遇到的问题, 会挑选一些翻译一下, 同时也结合自己的实例说一下看法. 有兴趣的可以看一下这个系列, 我相信会有一定帮助的.
如何不crash之3:通知
通常来说, 我更倾向于通知而不是KVO或者是绑定(译注1). 当KVO是最佳方案时, 我也会偶尔用KVO. 然而NSNotification, 与其它的陈年API一样, 简单易用且不容易crash. 但是你仍然要保持警惕.
(议: 最近刚好在KVO和通知之间进行了一番纠结, 现在网络上有一大堆的关于KVO和通知优劣的对比, 我只讲下自己的理解吧, KVO对keyPath依赖太重了, 监听一定要绑定到keypath, 而且KVO回调所携带的信息无法定制, 需要自己管理observer, 在属性可能发生变化的情况下, 比如, 你用了一个NSMutableDictionary来存储一些信息, 用KVO来监听这些信息的变化, 会显得非常棘手, 没有特殊情况, 还是推荐大家用NSNotification)
crash之路
当一个对象注册监听一个通知, 在dealloc的时候没有注销, 一旦这个通知被发出App就会crash. 这件事一定避免发生, 这篇文章接下来的部分会描述如何做到.(译注: 其实下面会讲到的不止这些, 不然我也不会专门翻译一下:) )
大的原则
我有一个简单,强硬且快速的原则: 通知只能在主线程发出(post), 没有例外. 如果代码运行在其它线程且需要发出一个通知, 那它需要在主线程发出这个通知.
这避免了所有通知到达你不期望的的线程造成的所有问题. 它避免了注销通知的竞态条件问题(译注2).
大部分的app代码都应该被执行到主线程. 用NSOperation或者GCD队列执行的代码应该与其它东西完全隔离, 当多个对象协作时, 应该用一个delegate模式或block(来传递数据).
总是在主线程发出通知也很容易. (我会写另外一篇关于线程和队列如何不crash的文章来阐述更多细节).
整体注销
有的人喜欢在dealloc中做一些额外的工作来为每一个通知注销, 就像下面一样:
[[NSNotificationCenter defaultCenter] removeObserver:self name:kSomeNotificationName object:someObject];
[[NSNotificationCenter defaultCenter] removeObserver:self name:kSomeOtherNotificationName object:someOtherObject];
等等...
你可以证明你写的这些代码都是对的, 但是需要再考虑一下, 你必须思考你的代码随着时间推移会变成什么样.
在以后, 你或者别人可能会增加另外一个通知, 但是忘记了调用removeObserver来为他的通知注销, 然后就可能发生crash. 另一个问题在于未来的开发者可能来审查你的代码, 还要确保以下每一个注册的通知都得到了移除. 这太痛苦了: 这完全是人工活, 并且很容易出错. 所以, 应该总是用下面的代码来取代上面:
[[NSNotificationCenter defaultCenter] removeObserver:self];
Indiana Jones 就是这么干的.
小心重复注册
如果一个对象注册了一个通知, 然后又注册了一次, 那么通知回调会被调用两次,这里并不会合并.(这种情况在过去iOS的viewDidLoad中经常发生. 开发者是应该把注册代码写在这里, 但是要记住, view可以被unload和reloaded, 这就意味着可能会出现对同样通知的多次注册).
(议: 总之, 弄明白哪些函数是可重入的, 哪些函数是不可重入的很重要, 如果是可重入的, 就一定要考虑重入的情况, 所以, 多测试内存警告的情况, 特别是接受通知的页面不在显示区域内的触发内存警告的情况)
你的通知处理代码应该要可以处理被重复调用的情况, 并且应该避免对给定对象重复注册同一个通知.
init时注册, dealloc时注销
在几乎所有的场景中, 我都在init方法中注册以及在dealloc中注销. 如果我发现一个对象添加和移除observation贯穿了整个对象的生命周期, 那我会考虑代码是否有强烈代码异味(strong code smell, 译注:所谓代码异味是指, 代码能够顺利完成其功能, 但是在逻辑和设计上却有很大的瑕疵, 比如很长的方法, 写重复的代码等等)
这是一个很好的机会(来审查): 要么它并不是真的需要这么干, 要么这个对象需要被拆分为更小的几个对象.
(议: 多多少少在开发阶段会遇到这样的问题, 要么是因为偷懒, 因为init不可重入, 直接在这里注册不需要考虑重复注册的问题, 要么是因为viewDidLoad的时机太晚, 需要更早的注册, 以免错过收到通知的正确时机. 其实目前我写的一块代码就有这样的情况, 用户自动登录, 一个view要监听登录成功的通知, 但是如果是在viewDidLoad里面写, 通知早就已经发出去了, 来不及接收, 仔细想想, 其实会有更好的解决办法的)
你知道对于一个给定的对象, init方法只会被调用一次, dealloc在没有其它引用的情况下也只会被调用一次. 你能利用这一点来保持注册和注销的平衡, 而不必
去思考或者追踪它, 多简单啊.
避免使用addObserverForName
有的人喜欢用-[NSNotificationCenter addObserverForName:object:queue:usingBlock:]
它看起来更加现代, 因为是基于block的, 我们都爱block.(我也是)
然而, 这不是个好主意, 你可能想把自己从编写通知处理方法中解脱, 但是你却做了一个更坏的打算, 因为现在你需要额外的工作来执行removeObserver:.因为这意味着没有整体注销, 你又回到了代码审查, 必须把另外一件事情也做对. 你喜欢把注册和通知处理的代码写在一起而喜欢这种基于block的方法版本, 但是在收拾结局和潜在crash上却要付出很大的代价.
译注
译注1: 这个绑定应该是OSX下面的东西, 作者在这个系列文章的第一部分有描述
译注2: Race Condition, 竞态条件: 多线程中比较难处理的一类问题, 百度百科了解更多
一些心得
原作者的一系列文章都还很不错的, 我挑一些这一篇是因为自己比较有感触, 遇到了很多类似的问题, 不代表别的就不好哟. 推荐大家都去看看, 在平时开发的时候多注意一些, 会减少很多潜在的问题.
再说一句
陆陆续续收到一些朋友的关注和喜欢, 心里比较开心也比较担心, 毕竟我从事从学到实际开发iOS才1年多, 以前在学校是专门做C和Linux方面的, 所以很多东西如果理解的不透彻, 就不敢拿出来给大家看, 这也暴露了自己还没有完整的知识体系, 希望和大家一起成长.
前段之间一直在忙开发, 以后有机会会更多的写一些文章发在这里, 可能这种形式的译文+自己的议论会的偏多, 毕竟纯正自己的东西还是要不断积累的. 最近也在看runtime的源码, 和<Inside C++ Object Model>这本书. 有机会的话和大家一起分享.