iOS 线程安全

Apple没有把 UIKit 设计为线程安全的类是有意为之的,将其打造为线程安全的话会使很多操作变慢。而事实上 UIKit 是和主线程绑定的,这一特点使得编写并发程序以及使用 UIKit 十分容易的,你唯一需要确保的就是对于 UIKit 的调用总是在主线程中来进行。

为什么 UIKit 不是线程安全的?

对于一个像 UIKit 这样的大型框架,确保它的线程安全将会带来巨大的工作量和成本。将 non-atomic 的属性变为 atomic 的属性只不过是需要做的变化里的微不足道的一小部分。通常来说,你需要同时改变若干个属性,才能看到它所带来的结果。为了解决这个问题,苹果可能不得不提供像 Core Data 中的 performBlock: 和 performBlockAndWait: 那样类似的方法来同步变更。另外你想想看,绝大多数对 UIKit 类的调用其实都是以配置为目的的,这使得将 UIKit 改为线程安全这件事情更显得毫无意义了。

然而即使是那些与配置共享的内部状态之类事情无关的调用,其实也不是线程安全的。如果你做过 iOS 3.2 或之前的黑暗年代的 app 开发的话,你肯定有过一边在后台准备图像时一边使用 NSString 的 drawInRect:withFont: 时的随机崩溃的经历。值得庆幸的事,在 iOS 4 中 苹果将大部分绘图的方法和诸如 UIColor 和 UIFont 这样的类改写为了后台线程可用

但不幸的是 Apple 在线程安全方面的文档是极度匮乏的。他们推荐只访问主线程,并且甚至是绘图方法他们都没有明确地表示保证线程安全。因此在阅读文档的同时,去读读 iOS 版本更新说明会是一个很好的选择。

对于大多数情况来说,UIKit 类确实只应该用在应用的主线程中。这对于那些继承自 UIResponder 的类以及那些操作你的应用的用户界面的类来说,不管如何都是很正确的。

内存回收 (deallocation) 问题

另一个在后台使用 UIKit 对象的的危险之处在于“内存回收问题”。Apple 在技术笔记 TN2109 中概述了这个问题,并提供了多种解决方案。这个问题其实是要求 UI 对象应该在主线程中被回收,因为在它们的 dealloc 方法被调用回收的时候,可能会去改变 view 的结构关系,而如我们所知,这种操作应该放在主线程来进行。

因为调用者被其他线程持有是非常常见的(不管是由于 operation 还是 block 所导致的),这也是很容易犯错并且难以被修正的问题。在 AFNetworking 中也一直长久存在这样的 bug,但是由于其自身的隐蔽性而鲜为人知,也很难重现其所造成的崩溃。在异步的 block 或者操作中一致使用 __weak,并且不去直接访问局部变量会对避开这类问题有所帮助。

GCD 的陷阱

对于大多数上锁的需求来说,GCD 就足够好了。它简单迅速,并且基于 block 的 API 使得粗心大意造成非平衡锁操作的概率下降了不少。然后,GCD 中还是有不少陷阱,我们在这里探索一下其中的一些。

将 GCD 当作递归锁使用

GCD 是一个对共享资源的访问进行串行化的队列。这个特性可以被当作锁来使用,但实际上它和 @synchronized 有很大区别。 GCD队列并非是可重入的,因为这将破坏队列的特性。很多有试图使用 dispatch_get_current_queue() 来绕开这个限制,但是这是一个糟糕的做法,Apple 在 iOS6 中将这个方法标记为废弃,自然也是有自己的理由。

// This is a bad idea.
inline void pst_dispatch_sync_reentrant(dispatch_queue_t queue, 
  dispatch_block_t block) 
{
    dispatch_get_current_queue() == queue ? block() 
                                          : dispatch_sync(queue, block);
}

对当前的队列进行测试也许在简单情况下可以行得通,但是一旦你的代码变得复杂一些,并且你可能有多个队列在同时被锁住的情况下,这种方法很快就悲剧了。一旦这种情况发生,几乎可以肯定的是你会遇到死锁。当然,你可以使用dispatch_get_specific(),这将截断整个队列结构,从而对某个特定的队列进行测试。要这么做的话,你还得为了在队列中附加标志队列的元数据,而去写自定义的队列构造函数。嘛,最好别这么做。其实在实用中,使用 NSRecursiveLock 会是一个更好的选择。

用 dispatch_async 修复时序问题

在使用 UIKit 的时候遇到了一些时序上的麻烦?很多时候,这样进行“修正”看来非常完美:

dispatch_async(dispatch_get_main_queue(), ^{
    // Some UIKit call that had timing issues but works fine 
    // in the next runloop.
    [self updatePopoverSize];
});

千万别这么做!相信我,这种做法将会在之后你的 app 规模大一些的时候让你找不着北。这种代码非常难以调试,并且你很快就会陷入用更多的 dispatch 来修复所谓的莫名其妙的"时序问题"。审视你的代码,并且找到合适的地方来进行调用(比如在 viewWillAppear 里调用,而不是 viewDidLoad 之类的)才是解决这个问题的正确做法。我在自己的代码中也还留有一些这样的 hack,但是我为它们基本都做了正确的文档工作,并且对应的 issue 也被一一记录过。

记住这不是真正的 GCD 特性,而只是一个在 GCD 下很容易实现的常见反面模式。事实上你可以使用performSelector:afterDelay: 方法来实现同样的操作,其中 delay 是在对应时间后的 runloop。

在性能关键的代码中混用 dispatchsync 和 dispatchasync

这个问题我花了好久来研究。在 PSPDFKit 中有一个使用了 LRU(最久未使用)算法列表的缓存类来记录对图片的访问。当你在页面中滚动时,这个方法将被调用非常多次。最初的实现使用了 dispatch_sync 来进行实际有效的访问,使用 dispatch_async 来更新 LRU 列表的位置。这导致了帧数远低于原来的 60 帧的目标。

当你的 app 中的其他运行的代码阻挡了 GCD 线程的时候,dispatch manager 需要花时间去寻找能够执行 dispatch_async 代码的线程,这有时候会花费一点时间。在找到合适的执行线程之前,你的同步调用就会被 block 住了。其实在这个例子中,异步情况的执行顺序并不是很重要,但没有能将这件事情告诉 GCD 的好办法。读/写锁这里并不能起到什么作用,因为在异步操作中基本上一定会需要进行顺序写入,而在此过程中读操作将被阻塞住。如果误用了 dispatch_async 代价将会是非常惨重的。在将它用作锁的时候,一定要非常小心。

使用 dispatch_async 来派发内存敏感的操作

我们已经谈论了很多关于 NSOperations 的话题了,一般情况下,使用这个更高层级的 API 会是一个好主意。当你要处理一段内存敏感的操作的代码块时,这个优势尤为突出、

在 PSPDFKit 的老版本中,我用了 GCD 队列来将已缓存的 JPG 图片写到磁盘中。当 retina 的 iPad 问世之后,这个操作出现了问题。ß因为分辨率翻倍了,相比渲染这张图片,将它编码花费的时间要长得多。所以,操作堆积在了队列中,当系统繁忙时,甚至有可能因为内存耗尽而崩溃。

我们没有办法追踪有多少个操作在队列中等待运行(除非你手动添加了追踪这个的代码),我们也没有现成的方法来在接收到低内存通告的时候来取消操作、这时候,切换到 NSOperations 可以使代码变得容易调试得多,并且允许我们在不添加手动管理的代码的情况下,做到对操作的追踪和取消。

当然也有一些不好的地方,比如你不能在你的 NSOperationQueue 中设置目标队列(就像 DISPATCH_QUEUE_PRIORITY_BACKGROUND之于 缓速 I/O 那样)。但这只是为了可调试性的一点小代价,而事实上这也帮助你避免遇到优先级反转的问题。我甚至不推荐直接使用已经包装好的 NSBlockOperation 的 API,而是建议使用一个 NSOperation 的真正的子类,包括实现其 description。诚然,这样做工作量会大一些,但是能输出所有运行中/准备运行的操作是及其有用的。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值