原文:苹果参考库“Network and Multitasking”
多任务是iOS4的关键特性。多任务允许你的应用在后台运行及挂起。对系统来说这是好事,但对你的应用来说会严重干扰其网络任务的执行。本文解释了在网络应用中如何处理好多任务。
本文大致由以下部分构成:
介绍
基础
监听 Socket
数据 Socket
实现细节
小心狗!
并发
Socket 回收测试
进阶
后台任务
续传
多任务中的特殊角色
后台执行
修订
介绍
多任务在iOS4开始引入,给网络应用增加了一定的复杂性。当iOS将你的程序放到了后台,程序可能很快就被挂起。此时程序中的代码不会再被执行。这将导致程序无法处理网络传来的数据。当程序被挂起时,系统有可能回收程序的socket资源,关闭socket连接。当然,iOS为网络应用与多任务兼容进行了专门的设计,以减少程序员的工作量,但在你的程序中仍然要做一些必要的处理。在基础部分,你将极大地改善程序的网络行为,以便与目前的多任务进行兼容,
本文首先描述了网络应用与多任务兼容的基本步骤(基础)。然后描述了具体的实现细节(实现细节),并向你展示在网络应用中充分利用多任务的好处(进阶)。
在此之前,你应当熟悉iOS app生命周期——见iOS Application Programming Guide。本文使用了该文档中定义的前台、后台及挂起等术语。
最后,本文适用于未使自己的程序与多任务进行兼容的开发人员。如果你的程序符合iOS Application Programming Guide中所提到的类别,你应当阅读Multitasking Superpowers一节。
基础
所有的iOS网络APIs最终都是基于BSD Socket实现的,因此应支持两种基本的sockets类型:
以下,针对每一种类型的sockets,对如何处理多任务进行了描述。
The followingsections describe how to handle multitasking for each type of socket.
监听Socket
对于多任务监听socket,处理非常简单:当程序进入后台后,应当关闭socket;当程序返回前台,重新打开socket。这有两个主要的原因:
记住,程序关闭监听socket的时候,也会停止socket上的Bonjour注册。
数据Socket
数据socket的情况要复杂一些,这取决于你的数据socket在做什么。当程序进入后台尤其是挂起时,你要决定是否关闭数据socket,还是让数据socket保持打开。关键取决于你的数据socket有多容易关闭和重新打开:
如果你想在程序进入后台后保持数据socket打开,你必须处理socket错误。错误处理是常见的,但这里变得尤其重要。因为程序挂起之后,socket资源可能会被内核回收,这样所有在该socket上的网络操作都会失败,这时你所能做的只有是关闭socket。
注意:
当你的程序恢复执行时,被回收资源的socket会返回一个错误,我们没有指明这个错误具体是什么,后面另做解释。但是,在许多情况下,这个错误你可能会想不到,它是EBADF!一般情况下EBADF意味着程序将一个无效的文件描述符传递给系统调用。在socket资源被回收的情况中,它其实并非文件描述符无效,而是socket不可用。
不幸的是,在这种情况下不可能用一个其他的、未被使用的错误编码,因为系统函数能够返回的错误编码已经被严格限制住了。
如果你通过数据socket使用的协议支持挂起(休眠)模式,你应当在进入后台时启动挂起模式,而在进入前台时关闭挂起模式。例如IMAP所支持的IDLE命令允许收件箱内容更新时向客户端发送异步通知。当你的程序挂起时,无法收到这些通知,因此当程序进入后台时你肯定想取消这些IDLE命令。
高级APIs
前面讨论了socket的内容,但实际上采用socket之上的上层协议就已足够。例如:
假设数据socket属于这些高层次结构,它的资源被内核回收,则这些高层结构会报告一个错误。你应该像处理所有其他网络错误一样检测并处理这个错误。例如:
实现细节
在你准备让你的程序对多任务切换进行响应时,本节描述一些值得注意重要事项。
小心狗!
如前所述,当你的程序从前台转到后台时,它可以继续一些重要的工作,主要是在applicationDidEnterBackground:委托方法中。最重要的是这些工作应当快速完成。如果程序在applicationDidEnterBackground:方法中花太长时间,它将被狗(系统进程)killed。
注意:
要进一步理解看门狗(watchdog),请阅读 Technical Note TN2151, 'Understanding and Analyzing iPhoneOS Application Crash Reports' 以及Technical Q&A QA1693, 'Synchronous Networking On TheMain Thread'。
在本文,任何涉及到网络等待的操作都属于“太长”之列。那是因为程序进入后台时网络立马会变得“反应迟钝”。因此程序在applicationDidEnterBackground:方法中会花去太长时间,直到最终变成狗的“口中食”。
也不是所有的网络操作都会等待。例如在applicationDidEnterBackground:方法中关闭socket监听;关闭socket监听总是很快。
甚至可以在这时传送数据。例如,如果你想发送一条指令让连接处于静默模式,这是可行的,因为指令被放在内核的socket缓存。在这种情况下,发送的命令仅仅拷贝数据到socket缓存中,而不会花费多少时间。
麻烦的是你的程序试图发送命令并等待响应。如果网络反应缓慢,响应受到延迟,你的程序将被狗killed。在必须等待网络的情况下,你应当使用后台任务(用[UIApplicationbeginBackgroundTaskWithExpirationHandler:])。例如,通过静默命令进入“静默”模式。则你的applicationDidEnterBackground:方法应该是:
这将让你的程序有额外时间去执行quiesce命令而不用在applicationDidEnterBackground方法中占用太多时间。
始终要记住,所有的后台任务必须提供一个超时handler,以便系统需要后台任务立即结束时调用。超时handler,例如applicationDidEnterBackground:委托方法,将由系统进程watchdog监管;如果一个超时handler花费太多时间,程序将被强行终止。如果你能保证网络操作不会等待,则它才能在超时handler(applicationDidEnterBackground:方法)中进行。你可以直接关闭一个数据socket,而不用通知远程端。
并发
如果你想在程序中使用了网络,很可能你要在程序退到后台的时候使用后台任务继续执行,哪怕是短暂的一小会。当你启动一个后台任务时,你必须提供一个超时handler以便取消后台任务。超时handler运行在主线程,它必须在主线程返回之前取消后台任务(因为你的程序马上就要被挂起或终止了)。而且正如前面提到的,超时handler必须快速完成,因为花费太长时间将被watchdog强制终止。
这导致了你在设计程序时有许多限制。最严重的是,在多线程中你无法使用同步(阻塞)网络APIs。这是因为多线程难以取消,而一个实现得好的超时handler需要很好地支持取消操作。
例如这种情况,一个以同步阻塞式的监听线程,它通常在接收系统调用时被阻塞。当用户按下Home按钮,程序被放到后台,你应该关闭socket。但你怎样解开在系统调用中被阻塞的线程?显然,没有什么好办法。
另外,考虑下这种情况。你的程序需要进行大数据的后台下载,但网络很慢,慢到程序已经用完了所有的后台任务执行时间。这时,系统开始在主线程中运行超时handler,然后程序在返回前关闭连接。你怎么处理正在等待读取数据的阻塞线程?同样,也没有什么好的办法。
因此,为了支持多任务,你可能得使用异步网络APIs。iOS提供各种类似的APIs——从底层APIs如GCD到高级APIs如NSURLConnection,还有许多中间层的APIs——我们鼓励你使用它们。
Run Loops
如果你使用一个基于网络API的run loop,为了给run loop完成所需的时间,你可能需要在一个超时handler中运行runloop。如果这样,你必须以自定义run loop模式去运行run loop;我们不支持在超时handler(applicationDidEnterBackground)中以默认模式运行runloop。记住,如果你使用这种技术,你必须定制在默认模式中的所有相关的run loop源。
Socket回收测试
如果你想编写代码处理将被内核回收的socket资源,你应当进行一些测试。系统对socket资源进行回收的细节是故意”undocumented”的,这种情况未来可能会有改变。但在iOS4.0-4.3系统中,以下行为将导致系统回收socket资源:
当你的程序恢复到前台运行时,程序的sockets就已经被销毁。
进阶
如果你的app要运行长时间的网络任务,你可以通过2件事情来改善用户体验:
后台任务
后台任务的常见应用之一就是避免网络传输的中断。如果用户开始大文件传输并切换到其他程序,后台任务能继续在后台进行。如果一切顺利,当用户返回程序,文件传输应当完成。要在你的app中实现后台任务,不需要你将逻辑划分为“前台的”和“后台的”两部分。每当程序开始一个耗时操作时就启动后台任务是一种比较好的做法,哪怕程序是在前台运行。当程序在前台时,后台任务没有任何影响;但当用户将程序切换到后台,后台任务的存在能让程序持续运行直至任务完成。
断点续传
如果你要长时间运行一个后台传输任务,那么支持断点续传将是至关重要的。断点续传在以下情况下尤其有用:
所有这些情况都能用断点续传解决。实现断点续传非常容易。如果是HTTP协议,大部分服务器都支持enitty标签和byteranges,这是断点下载的前提。而且NSURLConnection能利用这些特性。你可以参考HTTP协议规范(RFC 2616)。断点上传需要更高的技巧,这也依赖于上传的服务器。如果不修改服务器代码是不可能支持断点上传的。
实现FTP的断点下载也很容易;你可以在用CFFTPStream开始下载时指明kCFStreamPropertyFTPFileTransferOffset属性。CFFTPStream不支持FTP 断点上传 (r. 4086998) 。另外,FTP对安全的支持非常少(用户名、密码和数据都采用明文传输),如今在internet中仍然使用FTP进行上传是难以想象的。
多任务系统中,有一些app拥有特殊的地位。例如,音频播放器可以在后台播放而不会被挂起。本节介绍这些“特殊角色”和网络之间的关联。其中,与网络相关的两类特殊角色是:
本节对这两者进行了讨论。
后台执行
一些最常见的特殊的多任务程序能在后台执行代码。例如,音乐播放器在播放时,能在后台中不运行而不会被挂起。诸多文档中,“退到后台”一词是“可以被挂起”的同义词。这是一个好的经验法则,但这个规则对于那些能在后台执行的程序就不适用了。对于这些程序而言,退到后台并不意味着程序会就会被挂起。只有在首先停止了那些让它不被挂起的任务之后,它才会成为“可以被挂起”。继续以音乐播放器为例子,只有它停止播放后才会变得“可以被挂起”。
注意,这和后台任务(在“小心狗!”和“进阶”中讨论的)是不一样的。任何app都可在有限时间内以后台任务的方式执行代码。当后台任务完成后,这些程序就成为“可以挂起”。
一个VoIP(IP语音)程序是被设计为持续运行的,以便它可以监听VoIP控制连接;但是且为了尽可能减少对系统内存的占用,当它处于不激活状态时它是被挂起的。为此,VoIP程序必须为控制连接注册数据socket。注册的socket有两个特点:
更多关于如何创建VoIP程序,请参考iOS Application Programming Guide。