网络及多任务


原文:苹果参考库“Network and Multitasking”

多任务是iOS4的关键特性。多任务允许你的应用在后台运行及挂起。对系统来说这是好事,但对你的应用来说会严重干扰其网络任务的执行。本文解释了在网络应用中如何处理好多任务。

本文大致由以下部分构成:

介绍

基础

监听 Socket

数据 Socket

高级 APIs

实现细节

小心狗!

并发

Run Loops

Socket 回收测试

进阶

后台任务

续传

多任务中的特殊角色

后台执行

VoIP Sockets

修订

介绍

多任务在iOS4开始引入,给网络应用增加了一定的复杂性。当iOS将你的程序放到了后台,程序可能很快就被挂起。此时程序中的代码不会再被执行。这将导致程序无法处理网络传来的数据。当程序被挂起时,系统有可能回收程序的socket资源,关闭socket连接。当然,iOS为网络应用与多任务兼容进行了专门的设计,以减少程序员的工作量,但在你的程序中仍然要做一些必要的处理。在基础部分,你将极大地改善程序的网络行为,以便与目前的多任务进行兼容,

本文首先描述了网络应用与多任务兼容的基本步骤(基础)。然后描述了具体的实现细节(实现细节),并向你展示在网络应用中充分利用多任务的好处(进阶)。

在此之前,你应当熟悉iOS app生命周期——见iOS Application Programming Guide。本文使用了该文档中定义的前台、后台及挂起等术语。

最后,本文适用于未使自己的程序与多任务进行兼容的开发人员。如果你的程序符合iOS Application Programming Guide中所提到的类别,你应当阅读Multitasking Superpowers一节。

基础

所有的iOS网络APIs最终都是基于BSD Socket实现的,因此应支持两种基本的sockets类型:

  • 监听 sockets —用于接收TCP连接请求。
  • 数据 sockets — 用于通过网络传送数据。数据sockets通常代表了一个TCP连接,或者用于访问UDP端口。

以下,针对每一种类型的sockets,对如何处理多任务进行了描述。

The followingsections describe how to handle multitasking for each type of socket.

监听Socket

对于多任务监听socket,处理非常简单:当程序进入后台后,应当关闭socket;当程序返回前台,重新打开socket。这有两个主要的原因:

  • 当你的应用进入后台后,它会被挂起。一旦被挂起,它无法处理监听socket上的客户端连接。但内核仍然认为socket是有效的。如果客户端连接到socket,内核会接收该连接但实际上你的程序已经不能用它来通信了。虽然最终客户端会放弃,但会浪费一小点时间。因此,当程序到后台的时候关闭socket是更好的做法。因为客户端连接会被内核立即拒绝。
  • 如果系统挂起你的程序,稍后会回收监听端口上的资源,你的程序哪怕恢复运行,也不能再监听了。程序有可能会被通知,也有可能不被通知,这取决于它是如何管理监听socket的。一般避免这种情况的简单做法,就是在程序进入后台时完全关闭监听socket。

记住,程序关闭监听socket的时候,也会停止socket上的Bonjour注册。

数据Socket

数据socket的情况要复杂一些,这取决于你的数据socket在做什么。当程序进入后台尤其是挂起时,你要决定是否关闭数据socket,还是让数据socket保持打开。关键取决于你的数据socket有多容易关闭和重新打开:

  • 如果重新打开socket是简单的,代价低廉的,也没有用户可以察觉的后果,最好在程序进入后台时关闭socket,然后在返回前台时重新打开。这些都很简单,如果是这样,你可以忽略后边的内容。
  • 如果重新打开socket是缓慢的、代价高昂的,或者用户能够察觉,你可能想在程序进入后台后保持打开。这种情况一般出现在:用户从你的程序切换到另一个程序,然后又快速地切换回来,你不想让用户受到不必要的网络延迟的影响。但是,如果你保持socket打开,你必须做一些处理,如后面的内容所述。

如果你想在程序进入后台后保持数据socket打开,你必须处理socket错误。错误处理是常见的,但这里变得尤其重要。因为程序挂起之后,socket资源可能会被内核回收,这样所有在该socket上的网络操作都会失败,这时你所能做的只有是关闭socket。

注意:

当你的程序恢复执行时,被回收资源的socket会返回一个错误,我们没有指明这个错误具体是什么,后面另做解释。但是,在许多情况下,这个错误你可能会想不到,它是EBADF!一般情况下EBADF意味着程序将一个无效的文件描述符传递给系统调用。在socket资源被回收的情况中,它其实并非文件描述符无效,而是socket不可用。

不幸的是,在这种情况下不可能用一个其他的、未被使用的错误编码,因为系统函数能够返回的错误编码已经被严格限制住了。

如果你通过数据socket使用的协议支持挂起(休眠)模式,你应当在进入后台时启动挂起模式,而在进入前台时关闭挂起模式。例如IMAP所支持的IDLE命令允许收件箱内容更新时向客户端发送异步通知。当你的程序挂起时,无法收到这些通知,因此当程序进入后台时你肯定想取消这些IDLE命令。

高级APIs

前面讨论了socket的内容,但实际上采用socket之上的上层协议就已足够。例如:

  • 假设你使用NSStream去管理TCP连接,则NSStream自动桥接到CFStream(准确地说,是CFStream一个具体的子类CFSocketStream),CFStream负责管理底层socket。
  • 假设你使用了NSURLConnection,则它用一个CFSTream(CFHTTPStream)实现,转而用另一个CFSTream(这次是CFSocketStream)管理底层socket。

假设数据socket属于这些高层次结构,它的资源被内核回收,则这些高层结构会报告一个错误。你应该像处理所有其他网络错误一样检测并处理这个错误。例如:

  • 假设你使用NSStream,你会收到NSStreamEventHasBytesAvailable事件。你可以在读取流时进行处理,[NSInputStream –read:maxLength:]会返回-1,表明有错误发生。通过调用[NSStream streamError]你可以获得准确的错误。
  • 假设你使用NSURLConnection,连接对象将通过调用connection:didFailWithError:委托方法指出这个错误。

实现细节

在你准备让你的程序对多任务切换进行响应时,本节描述一些值得注意重要事项。

小心狗!

如前所述,当你的程序从前台转到后台时,它可以继续一些重要的工作,主要是在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:方法应该是:

  1. 启动一个异步线程发送quiesce命令并等待响应
  2. 启动一个后台任务,申请额外的时间以完成任务
  3. 返回

这将让你的程序有额外时间去执行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资源:

  1. 把程序转入到后台;
  2. 让程序挂起;
  3. 锁屏;

当你的程序恢复到前台运行时,程序的sockets就已经被销毁。

进阶

如果你的app要运行长时间的网络任务,你可以通过2件事情来改善用户体验:

  • 使用后台任务来进行网络传输;
  • 实现可恢复的传输(断点续传)

后台任务

后台任务的常见应用之一就是避免网络传输的中断。如果用户开始大文件传输并切换到其他程序,后台任务能继续在后台进行。如果一切顺利,当用户返回程序,文件传输应当完成。要在你的app中实现后台任务,不需要你将逻辑划分为“前台的”和“后台的”两部分。每当程序开始一个耗时操作时就启动后台任务是一种比较好的做法,哪怕程序是在前台运行。当程序在前台时,后台任务没有任何影响;但当用户将程序切换到后台,后台任务的存在能让程序持续运行直至任务完成。

断点续传

如果你要长时间运行一个后台传输任务,那么支持断点续传将是至关重要的。断点续传在以下情况下尤其有用:

  • 设备不支持多任务;
  • 在传输过程中网络中断;
  • 程序请求了后台任务,然而被系统拒绝(即[UIApplication beginBackgroundTaskWithExpirationHandler:] 方法返回了 UIBackgroundTaskInvalid);
  • 系统资源变少,系统必须挂起和终止应用程序,此时后台任务还未完成;
  • 传输任务需要更多时间,而后台任务可用的时间已经不够了。
  • if the transfer takes more time than the background task will allow

所有这些情况都能用断点续传解决。实现断点续传非常容易。如果是HTTP协议,大部分服务器都支持enitty标签和byteranges,这是断点下载的前提。而且NSURLConnection能利用这些特性。你可以参考HTTP协议规范(RFC 2616)。断点上传需要更高的技巧,这也依赖于上传的服务器。如果不修改服务器代码是不可能支持断点上传的。

实现FTP的断点下载也很容易;你可以在用CFFTPStream开始下载时指明kCFStreamPropertyFTPFileTransferOffset属性。CFFTPStream不支持FTP 断点上传 (r. 4086998) 。另外,FTP对安全的支持非常少(用户名、密码和数据都采用明文传输),如今在internet中仍然使用FTP进行上传是难以想象的。

多任务中的特殊角色

多任务系统中,有一些app拥有特殊的地位。例如,音频播放器可以在后台播放而不会被挂起。本节介绍这些“特殊角色”和网络之间的关联。其中,与网络相关的两类特殊角色是:

  • 后台运行代码
  • VoIP sockets

本节对这两者进行了讨论。

后台执行

一些最常见的特殊的多任务程序能在后台执行代码。例如,音乐播放器在播放时,能在后台中不运行而不会被挂起。诸多文档中,“退到后台”一词是“可以被挂起”的同义词。这是一个好的经验法则,但这个规则对于那些能在后台执行的程序就不适用了。对于这些程序而言,退到后台并不意味着程序会就会被挂起。只有在首先停止了那些让它不被挂起的任务之后,它才会成为“可以被挂起”。继续以音乐播放器为例子,只有它停止播放后才会变得“可以被挂起”。

注意,这和后台任务(在“小心狗!”和“进阶”中讨论的)是不一样的。任何app都可在有限时间内以后台任务的方式执行代码。当后台任务完成后,这些程序就成为“可以挂起”。

VoIP Sockets

一个VoIP(IP语音)程序是被设计为持续运行的,以便它可以监听VoIP控制连接;但是且为了尽可能减少对系统内存的占用,当它处于不激活状态时它是被挂起的。为此,VoIP程序必须为控制连接注册数据socket。注册的socket有两个特点:

  • 当程序挂起时,系统会监控程序的socket动作。如果有数据送到socket,系统恢复app的执行(尽管在后台),以便读取数据并采取适当的动作(例如,通知用户有来电)。
  • socket资源不会被回收。因此,程序挂起后不需要担心其socket被销毁。

更多关于如何创建VoIP程序,请参考iOS Application Programming Guide


你可能感兴趣的:(网络)