常见的后台实现
文章目录
【注意】最后更新于 September 24, 2017,文中内容可能已过时,请谨慎使用。
概览
本文主要探讨一些常用后台任务的最佳实践:
- 如何做异步网络请求
- 如何异步处理大型文件,以保持较低的内存占用
操作队列 (Operation Queues) 还是 GCD ?
操作队列提供了在 GCD 中不那么容易复制的有用特性。其中最重要的一个就是可以取消在任务处理队列中的任务,而且操作队列在管理操作间的依赖关系方面也容易一些。 GCD 给予你更多的控制权力以及操作队列中所不能使用的底层函数。详细介绍可以参考底层并发 API 这篇文章。
后台 UI 代码
首先要强调:UIKit 只能在主线程上运行。而那部分不与 UIKit 直接相关,却会消耗大量时间的 UI 代码可以被移动到后台去处理,以避免其将主线程阻塞太久。
后台获取UI数据
例如使用操作队列隔离以下昂贵操作:
|
|
如你所见,这些代码其实一点也不直接明了。我们首先声明了一个 weak 引用来参照 self,否则会形成循环引用( block 持有了 self,私有的 operationQueue
retain 了 block,而 self 又 retain 了 operationQueue
)。为了避免在运行 block 时访问到已被释放的对象,在 block 中我们又需要将其转回 strong 引用。
这在 ARC 和 block 主导的编程范式中是解决 retain cycle 的一种常见也是最标准的方法。
后台绘制UI
如果你确定 drawRect:
是你的应用的性能瓶颈,那么你可以将这些绘制代码放到后台去做。但是在你这样做之前,检查下看看是不是有其他方法来解决,比如、考虑使用 core animation layers 或者预先渲染图片而不去做 Core Graphics 绘制。
如果你确实认为在后台执行绘制代码会是你的最好选择时再这么做。其实解决起来也很简单,把 drawRect:
中的代码放到一个后台操作中去做就可以了。然后将原本打算绘制的视图用一个 image view 来替换,等到操作执行完后再去更新。在绘制的方法中,使用 UIGraphicsBeginImageContextWithOptions
来取代 UIGraphicsGetCurrentContext
:
|
|
通过在第三个参数中传入 0 ,设备的主屏幕的 scale 将被自动传入,这将使图片在普通设备和 retina 屏幕上都有良好的表现。
cell在操作队列中异步绘制
如果你在 table view 或者是 collection view 的 cell 上做了自定义绘制的话,最好将它们放入 operation 的子类中去。你可以将它们添加到后台操作队列,也可以在用户将 cell 滚动出边界时的 didEndDisplayingCell
委托方法中进行取消。这些技巧都在 2012 年的WWDC Session 211 – Building Concurrent User Interfaces on iOS中有详细阐述。
其他方案
除了在后台自己调度绘制代码,以也可以试试看使用 CALayer
的 drawsAsynchronously
属性。然而你需要精心衡量这样做的效果,因为有时候它能使绘制加速,有时候却适得其反。
异步网络请求处理
你的所有网络请求都应该采取异步的方式完成。
然而,在 GCD 下,有时候你可能会看到这样的代码
|
|
乍看起来没什么问题,但是这段代码却有致命缺陷。你没有办法去取消这个同步的网络请求。它将阻塞主线程直到它完成。如果请求一直没结果,那就只能干等到超时(比如 dataWithContentsOfURL:
的超时时间是 30 秒)。
分析状况
- 当队列是串行执行时,它将一直被阻塞住。
- 当队列是并行执行时,GCD 需要重开一个线程来补凑你阻塞住的线程。
两种结果都不太妙,所以最好还是不要阻塞线程。
方案一
要解决上面的困境,我们可以使用 NSURLConnection
的异步方法,并且把所有操作转化为 operation 来执行。通过这种方法,我们可以从操作队列的强大功能和便利中获益良多:我们能轻易地控制并发操作的数量,添加依赖,以及取消操作。
例如:在NSOperation
子类DownloadOperation
中重写start
方法,并实现NSURLConnectionDelegate
代理方法。
|
|
然而,在这里还有一些事情值得注意: NSURLConnection
是通过 run loop 来发送事件的。因为发送事件不会花多少时间,因此最简单的是就只使用 main run loop 来做这个。然后,我们就可以用后台线程来处理输入的数据了。
方案二
另一种可能的方式是使用像 AFNetworking 这样的框架:建立一个独立的线程,为建立的线程设置自己的 run loop,然后在其中调度 URL 连接。但是并不推荐你自己去实现这些事情。
要处理URL 连接,我们重写自定义的 operation 子类中的 start
方法:
|
|
由于重写的是 start
方法,所以我们需要自己要管理操作的 isExecuting
和 isFinished
状态。要取消一个操作,我们需要取消 connection ,并且设定合适的标记,这样操作队列才知道操作已经完成。
|
|
当连接完成加载后,它向代理发送回调:
|
|
就这么多了。完整的代码可以参见GitHub上的示例工程。
总结来说,我们建议要么你花时间来把事情做对做好,要么就直接使用像 AFNetworking 这样的框架。其实 AFNetworking 还提供了不少好用的小工具,比如有个 UIImageView
的 category,来负责异步地从一个 URL 加载图片。在你的 table view 里使用的话,还能自动帮你处理取消加载操作,非常方便。
扩展阅读:
- Concurrency Programming Guide
- NSOperation Class Reference: Concurrent vs. Non-Concurrent Operations
- Blog: synchronous vs. asynchronous NSURLConnection
- GitHub:
SDWebImageDownloaderOperation.m
- Blog: Progressive image download with ImageIO
- WWDC 2012 Session 211: Building Concurrent User Interfaces on iOS
进阶:后台文件 I/O
构建一个类,负责一行一行读取文件而不是一次将整个文件读入内存,另外要在后台队列处理文件,以保持应用相应用户的操作。
为了达到这个目的,我们使用能让我们异步处理文件的 NSInputStream
。根据官方文档的描述:
如果你总是需要从头到尾来读/写文件的话,streams 提供了一个简单的接口来异步完成这个操作
不管你是否使用 streams,大体上逐行读取一个文件的模式是这样的:
- 建立一个中间缓冲层以提供,当没有找到换行符号的时候可以向其中添加数据
- 从 stream 中读取一块数据
- 对于这块数据中发现的每一个换行符,取中间缓冲层,向其中添加数据,直到(并包括)这个换行符,并将其输出
- 将剩余的字节添加到中间缓冲层去
- 回到 2,直到 stream 关闭
为了将其运用到实践中,我们又建立了一个示例应用,里面有一个 Reader
类完成了这件事情,它的接口十分简单
|
|
runloop分发NSInputStream事件
注意,这个类不是 NSOperation 的子类。与 URL connections 类似,输入的 streams 通过 run loop 来传递它的事件。这里,我们仍然采用 main run loop 来分发事件,然后将数据处理过程派发至后台操作线程里去处理。
|
|
调用时的代码:
|
|
NSInputStream代理方法
现在,input stream 将(在主线程)向我们发送代理消息,然后我们可以在操作队列中加入一个 block 操作来执行处理了:
|
|
缓冲区处理
处理数据块的过程是先查看当前已缓冲的数据,并将新加入的数据附加上去。接下来它将按照换行符分解成小的部分,并逐行处理。 数据处理过程中会不断的从buffer中获取已读入的数据。然后把这些新读入的数据按行分开并存储。剩余的数据被再次存储到缓冲区中:
|
|
现在你运行示例应用的话,会发现它在响应事件时非常迅速,内存的开销也保持很低(在我们测试时,不论读入的文件有多大,堆所占用的内存量始终低于 800KB)。绝大部分时候,使用逐块读入的方式来处理大文件,是非常有用的技术。
延伸阅读:
- File System Programming Guide: Techniques for Reading and Writing Files Without File Coordinators
- StackOverflow: How to read data from NSFileHandle line by line?
总结
通过我们所列举的几个示例,我们展示了如何异步地在后台执行一些常见任务。在所有的解决方案中,我们尽力保持了代码的简单,这是因为在并发编程中,稍不留神就会捅出篓子来。
很多时候为了避免麻烦,你可能更愿意在主线程中完成你的工作,在你能这么做事,这确实让你的工作轻松不少,但是当你发现性能瓶颈时,你可以尝试尽可能用最简单的策略将那些繁重任务放到后台去做。
我们在上面例子中所展示的方法对于其他任务来说也是安全的选择。在主队列中接收事件或者数据,然后用后台操作队列来执行实际操作,然后回到主队列去传递结果,遵循这样的原则来编写尽量简单的并行代码,将是保证高效正确的不二法则。
原文 Common Background Practices
译文 常见的后台实践
文章作者 iTBoyer
上次更新 2017-09-24