iPhone和iPad的企业应用开发之错误处理

移动开发 iOS
到目前为止,我们所介绍的iPhone与其他系统的网络交互都是基于一切正常这个假设。本章将会放弃这个假设,并深入探究网络的真实世界。在真实世界中,事情是会出错的,有时可能是非常严重的错误:手机进入与离开网络、包丢掉或是延迟;网络基础设施出错;偶尔用户还会出错。如果一切正常,那么编写iOS应用就会简单不少,不过遗憾的是现实并非如此。本章将会探讨导致网络操作失败的几个因素,介绍系统如何将失败情况告知应用,应用又该如何优雅地通知用户。此外,本章还将介绍如何在不往应用逻辑中添加错误处理代码的情况下,以一种整洁且一

5.1  理解错误源

早期的iOS有个很棒的天气预报应用。它在Wi-Fi和信号良好的蜂窝网络下使用正常,不过当网络质量不那么好时,这个天气预报应用就像感冒似的,在主屏幕上崩溃。有不少应用在出现网络错误时表现很差劲,会疯狂弹出大量UIAlertView以告诉用户出现了“404 Error on Server X”等类似信息。还有很多应用在网络变慢时界面会变得没有响应。这些情况的出现都是没有很好地理解网络失败模式以及没有预期到可能的网络降级或是失败。如果想要避免这类错误并能够充分地处理网络错误,那么你首先需要理解它们的起源。

考虑一个字节是如何从设备发往远程服务器以及如何从远程服务器将这个字节接收到设备,这个过程只需要几百毫秒的时间,不过却要求网络设备都能正常工作才行。设备网络与网络互联的复杂性导致了分层网络的产生。分层网络将这种复杂环境划分成了更加易于管理的模块。虽然这对程序员很有帮助,不过当数据在各个层之间流动时可能会产生之前提到的网络错误。图5-1展示了Internet协议栈的各个层次。

  每一层都会执行某种错误检测,这可能是数学意义上的、逻辑意义上的,或是其他类型的检测。比如,当网络接口层接收到某一帧时,它首先会通过错误校正码来验 证内容,如果不匹配,那么错误就产生了。如果这个帧根本就没有到达,那就会产生超时或是连接重置。错误检测出现在栈的每一层,自下而上直到应用层,应用层 则会从语法和语义上检查消息。

     在使用iOS中的URL加载系统时,虽然手机与服务器之间的连接可能会出现各种各样的问题,不过可以将这些原因分成3种错误类别,分别是操作系统错误、 HTTP错误与应用错误。这些错误类别与创建HTTP请求的操作序列相关。图5-2展示了向应用服务器发出的HTTP请求(提供来自于企业网络的一些数 据)的简单序列图。每块阴影区域都表示这3种错误类型的错误域。典型地,操作系统错误是由HTTP服务器问题导致的。HTTP错误是由HTTP服务器或应 用服务器导致的。应用错误是由请求传输的数据或应用服务器查询的其他系统导致的。

 

  如果请求是安全的HTTPS请求,或是HTTP服务器被重定向客户端,那么上面这个序列的步骤将会变得更加复杂。上述很多步骤都包含着大量的子步骤,比如在建立TCP连接时涉及的SYN与SYN-ACK包序列等。下面将会详细介绍每一种错误类别。

5.1.1  操作系统错误

    操作系统错误是由数据包没有到达预定目标导致的。数据包可能是建立连接的一部分,也可能位于连接建立的中间阶段。OS错误可能由如下原因造成:

没有网络——如果设备没有数据网络连接,那么连接尝试很快就会被拒绝或是失败。这些类型的错误可以通过Apple提供的Reachability框架检测到,本节后面将会对此进行介绍。

无法路由到目标主机——设备可能有网络连接,不过连接的目标可能位于隔离的网络中或是处于离线状态。这些错误有时可以由操作系统迅速检测到,不过也有可能导致连接超时。

 没有应用监听目标端口——在请求到达目标主机后,数据包会被发送到请求指定的端口号。如果没有服务器监听这个端口或是有太多的连接请求在排队,那么连接请求就会被拒绝。

无法解析目标主机名——如果无法解析目标主机名,那么URL加载系统就会返回错误。通常情况下,这些错误是由配置错误或是尝试访问没有外部名字解析且处于隔离网络中的主机造成的。

在iOS的URL加载系统中,操作系统错误会以NSError对象的形式发送给应用。iOS通过NSError在软件组件间传递错误信息。相比简单的错误代码来说,使用NSError的主要优势在于NSError对象包含了错误域属性。

不过,NSError对象的使用并不限于操作系统。应用可以创建自己的NSError对象,使用它们在应用内传递错误消息。如下代码片段展示的应用方法使用NSError向调用的视图控制器传递回失败信息:

 

  1. -(id)fetchMyStuff:(NSURL*)url error:(NSError**)error 
  2. BOOL errorOccurred = NO; 
  3.   
  4. // some code that makes a call and may fail 
  5.   
  6. if(errorOccurred) //some kind of error 
  7. NSMutableDictionary *errorDict = [NSMutableDictionary dictionary]; 
  8. [errorDictsetValue:@"Failed to fetch my stuff" 
  9. forKey:NSLocalizedDescriptionKey]; 
  10. *error = [NSErrorerrorWithDomain:@"myDomain" 
  11. code:kSomeErrorCode 
  12. userInfo:errorDict]; 
  13. return nil; 
  14. else { 
  15. return stuff 
  16.   

域 属性根据产生错误代码的库或框架对这些错误代码进行隔离。借助域,框架开发者无须担心覆盖错误代码,因为域属性定义了产生错误的框架。比如,框架A 与B 都会产生错误代码1,不过这两个错误代码会被每个框架提供的唯一域值进行区分。因此,如果代码需要区分NSError 值,就必须对NSError 对象的code 与domain 属性进行比较。

NSError 对象有如下3 个主要属性:
code——标识错误的NSInteger 值。对于产生该错误的错误域来说,这个值是唯一的。
domain —— 指定错误域的NSString 指针, 比如NSPOSIXErrorDomain 、NSOSStatusErrorDomain 及NSMachErrorDomain。
userInfo——NSDictionary 指针,其中包含特定于错误的值。
URL 加载系统中产生的很多错误都来自于NSURLErrorDomain 域,代码值基本上都来自于CFNetworkErrors.h 中定义的错误代码。与iOS 提供的其他常量值一样,代码应该使用针对错误定义好的常量名而不是实际的错误代码值。比如,如果客户端无法连接到主机,那么错误代码是1004,并且有定 义好的常量kCFURLErrorCannotConnectToHost。代码绝不应该直接引用1004,因为这个值可能会在操作系统未来的修订版中发 生变化;相反,应该使用提供的枚举名kCFURLError。
      如下是使用URL 加载系统创建HTTP 请求的代码示例:

 

  1. <span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">NSHTTPURLResponse *response=nil; 
  2. NSError *error=nil; 
  3. NSData *myData=[NSURLConnectionsendSynchronousRequest:request 
  4. returningResponse:&response 
  5. error:&error]; 
  6. if (!error) { 
  7. // No OS Errors, keep going in the process 
  8. ... 
  9. else { 
  10. // Something low level broke 
  11. }</span></span> 

注 意,NSError 对象被声明为指向nil 的指针。如果出现错误,那么NSURLConnection对象只会实例化NSError 对象。URL 加载系统拥有NSError 对象;如果稍后代码会用到它,那么应该保持这个对象。如果在同步请求完成后NSError 指针依然指向nil,那就说明没有产生底层的OS 错误。这时,代码就知道没有产生OS 级别的错误,不过错误可能出现在协议栈的某个高层。
如果应用创建的是异步请求,那么NSError 对象就会返回到委托类的下面这个方法:

  1. <span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">- (void)connection:(NSURLConnection *)connection 
  2. didFailWithError:(NSError *)error</span></span> 

这是传递给请求委托的最终消息,委托必须能识别出错误的原因并作出恰当的反应。在如下示例中,委托会向用户展UIAlertView:

 

  1. <span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">- (void) connection:conndidFailWithError:error { 
  2. UIAlertView *alert = [UIAlertViewalloc] initWithTitle:@"Network Error" 
  3. message:[error description] 
  4. delegate:self 
  5. cancelButtonTitle:@"Oh Well" 
  6. otherButtonTitles:nil]; 
  7. [alert show]; 
  8. [alert release]; 
  9. }</span></span> 

上述代 码以一种生硬且不友好的方式将错误展现给了用户。在iOS 人机界面指南(HiG)中,Apple 建议不要过度使用UIAlertViews,因为这会破坏设备的使用感受。5.3 节“优雅地处理网络错误”中介绍了如何通过良好的用户界面以一种干净且一致的方式处理错误的模式。
      iOS 设备通信错误的另一主要原因就是由于没有网络连接而导致设备无法访问目标服务器。可以在尝试发起网络连接前检查一下网络状态,这样可以避免很多OS 错误。请记
住,这些设备可能会很快地进入或是离开网络。因此,在每次调用前检查网络的可达性是非常合情合理的事情。
      iOS 的SystemConfiguration 框架提供了多种方式来确定设备的网络连接状态。可以在SCNetworkReachability 参考文档中找到关于底层API 的详尽信息。这个API 非常强大,不过也有点隐秘。幸好,Apple 提供了一个名为Reachability 的示例程序,它为SCNetworkReachability实现了一个简化、高层次的封装器。Reachability 位于iOS 开发者库中。
      Reachability 封装器提供如下4 个主要功能:

标识设备是否具备可用的网络连接
标识当前的网络连接是否可以到达某个特定的主机
标识当前使用的是哪种网络技术:Wi-Fi、WWAN 还是什么技术都没用
在网络状态发生变化时发出通知要想使用Reachability API,请从iOS 开发者库中下载示例程序,地址是http://developer.apple.com/library/ios/#samplecode /Reachability/Introduction/Intro.html,然后将Reachability.h与Reachability.m 添加到应用的Xcode 项目中。此外,还需要将SystemConfiguration 框架添加到Xcode 项目中。将SystemConfiguration 框架添加到Xcode 项目中需要编辑项目配置。图5-3 展示了将SystemConfiguration 框架添到Xcode 项目中所需的步骤。
(3) 选择SystemConfiguration.framework

  选定好项目目标后,找到设置中的Linked Frameworks and Libraries,单击+按钮添加框架,这时会出现框架选择界面。选择SystemConfiguration 框架,单击add 按钮将其添加到项目中。
      如下代码片段会检查是否存在网络连接。不保证任何特定的主机或IP 地址是可达的,只是标识是否存在网络连接。

  1. <span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">#import "Reachability.h" 
  2. ... 
  3. if([[Reachability reachabilityForInternetConnection] 
  4. currentReachabilityStatus] == NotReachable) { 
  5. // handle the lack of a network 
  6. }</span></span> 

     在某些情况下,你可能想要修改某些动作、禁用UI 元素或是当设备处于有限制的网络中时修改超时值。如果应用需要知道当前正在使用的连接类型,那么请使用如下代码:

  1. <span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">#import "Reachability.h" 
  2. ... 
  3. NetworkStatus reach = [[Reachability reachabilityForInternetConnection] 
  4. currentReachabilityStatus]; 
  5. if(reach == ReachableViaWWAN) { 
  6. // Network Is reachable via WWAN (aka. carrier network) 
  7. else if(reach == ReachableViaWiFi) { 
  8. // Network is reachable via WiFi 
  9. }</span></span> 

     知道设备可达性状态的变化也是很有必要的,这样就可以主动修改应用行为。如下代码片段启动对网络状态的监控:

  1. <span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">#import "Reachability.h" 
  2. ... 
  3. [[NSNotificationCenterdefaultCenter] 
  4. addObserver:self 
  5. selector:@selector(networkChanged:) 
  6. name:kReachabilityChangedNotification 
  7. object:nil]; 
  8. Reachability *reachability; 
  9. reachability = [[Reachability reachabilityForInternetConnection] retain]; 
  10. [reachability startNotifier];</span></span> 

上述代码将当前对象注册为通知观察者,名为kReachabilityChangedNotification。
NSNotificationCenter 会调用当前对象的名为networkChanged:的方法。当可达性状态发生变化时,就向该对象传递NSNotification 及新的可达性状态。如下示例展示了通知监听者:

  1. <span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">- (void) networkChanged: (NSNotification* )notification 
  2. Reachability* reachability = [notification object]; 
  3. 第Ⅱ部分 HTTP 请求:iOS 网络功能 
  4. 98 
  5. if(reachability == ReachableViaWWAN) { 
  6. // Network Is reachable via WWAN (a.k.a. carrier network) 
  7. else if(reachability == ReachableViaWiFi) { 
  8. // Network is reachable via WiFi 
  9. else if(reachability == NotReachable) { 
  10. // No Network available 
  11. }</span></span> 

  可达性还可以确定当前网络上某个特定的主机是否是可达的。可以通过该特性根据应用是处于内部隔离的网络上还是公开的Internet 上调整企业应用的行为。如下代码示例展示了该特性:

 

  1. <span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">Reachability *reach = [Reachability 
  2. reachabilityWithHostName:@"www.captechconsulting.com"]; 
  3. if(reachability == NotReachable) { 
  4. // The target host is not reachable available 
  5. }</span></span> 

  请记住,该特性对目标主机的访问有个来回。如果每个请求都使用该特性,那就会极大增加应用的网络负载与延迟。Apple 建议不要在主线程上检测主机的可达性,因为尝试访问主机可能会阻塞主线程,这会导致UI 被冻结。
      OS 错误首先就表明请求出现了问题。应用开发者有时会忽略掉它们,不过这样做是有风险的。因为HTTP 使用了分层网络,这时HTTP 层或是应用层可能会出现其他类型的潜在失败情况。

#p#

5.1.2 HTTP 错误
HTTP 错误是由HTTP 请求、HTTP 服务器或应用服务器的问题造成的。HTTP 错误通过HTTP 响应的状态码发送给请求客户端。
404 状态是常见的一种HTTP 错误,表示找不到URL 指定的资源。下述代码片段中的HTTP 头就是当HTTP 服务器找不到请求资源时给出的原始输出:

 

  1. <span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">HTTP/1.1 404 Not Found 
  2. Date: Sat, 04 Feb 2012 18:32:25 GMT 
  3. Server: Apache/2.2.14 (Ubuntu) 
  4. Vary: Accept-Encoding 
  5. Content-Encoding: gzip 
  6. Content-Length: 248 
  7. Keep-Alive: timeout=15, max=100 
  8. Connection: Keep-Alive 
  9. Content-Type: text/html; charset=iso-8859-1</span></span> 

响应的***行有状态码。HTTP 响应可以带有消息体,其中包含友好、用户可读的信息,用于描述发生的事情。你不应该将是否有响应体作为判断HTTP 请求成功与否的标志。
一共有5 类HTTP 错误:

● 信息性质的100 级别——来自于HTTP 服务器的信息,表示请求的处理将会继续,不过带有警告。
● 成功的200 级别——服务器处理了请求。每个200 级别的状态都表示成功请求的不同结果。比如,204 表示请求成功,不过没有向客户端返回负载。
● 重定向需要的300 级别——表示客户端必须执行某个动作才能继续请求,因为所需的资源已经移动了。URL 加载系统的同步请求方法会自动处理重定向而无须通知代码。如果应用需要对重定向进行自定义处理,那么应该使用异步请求。
● 客户端错误400 级别——表示客户端发出了服务器无法正确处理的错误数据。比如,未知的URL 或是不正确的HTTP 头会导致这个范围内的错误。
● 下游错误500 级别——表示HTTP 服务器与下游应用服务器之间出现了错误。比如,如果Web 服务器调用了JavaEE 应用服务器,Servlet 出现了NullPointerException,那么客户端就会收到500 级别的错误。
      iOS 中的URL 加载系统会处理HTTP 头的解析,并可以轻松获取到HTTP 状态。如果代码通过HTTP 或HTTPS URL 发出了同步调用,那么返回的响应对象就是一个NSHTTPURLResponse 实例。NSHTTPURLResponse 对象的statusCode 属性会返回数值形式的请求的HTTP 状态。如下代码演示了对NSError 对象以及从HTTP 服务器返回的成功状态的验证:

  1. <span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">NSHTTPURLResponse *response=nil; 
  2. NSError *error=nil; 
  3. NSData *myData = [NSURLConnectionsendSynchronousRequest:request 
  4. returningResponse:&response 
  5. error:&error]; 
  6. //Check the return 
  7. if((!error) && ([response statusCode] == 200)) { 
  8. // looks like things worked 
  9. else { 
  10. // things broke, again. 
  11. }</span></span> 

  如果请求的URL不是HTTP,那么应用就应该验证响应对象是否是NSHTTPURLResponse对象。验证对象类型的***方法是使用返回对象的isKindOfClass:方法,如下所示:

  1. <span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">if([response isKindOfClass:[NSHTTPURLResponse class]]) { 
  2. // It is a HTTP response, so we can check the status code 
  3. ...</span></span> 

要想了解关于HTTP 状态码的权威信息,请参考W3 RFC 2616,网址是http://www.w3.org/Protocols/rfc2616/rfc2616.html。
5.1.3 应用错误
      本节将会介绍网络协议栈的下一层(应用层)产生的错误。应用错误不同于OS 错误或HTTP 错误,因为并没有针对这些错误的标准值或是原因的集合。这些错误是由运行在服
务层之上的业务逻辑和应用造成的。在某些情况下,错误可能是代码问题,比如异常,不过在其他一些情况下,错误可能是语义错误,比如向服务提供了无效的账号等。对于前者来说,建议生成HTTP 500 级别的错误;对于后者来说,应该在应用负载中返回错误码。
      比如,如果用户尝试从账户中转账的金额超出了账户的可用余额,那么手机银行就应该报告应用错误。如果发出了这样的请求,那么OS 会说请求成功发送并接收到了响应。HTTP 服务器会报告接收到了请求并发出了响应,不过应用层必须报告这笔交易失败。报告应用错误的***实践是将应用的负载数据封装在标准信封中,信封中含有一致的 应用错误位置信息。在上述资金转账示例中,成功的转账响应的业务负载应该如下所示:

  1. <span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">{ "transferResponse":{ 
  2. "fromAccount":1
  3. "toAccount":5
  4. "amount":500.00
  5. "confirmation":232348844 
  6. }</span></span> 
  7.  
  8. <span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;"
  9. </span></span> 

响 应包含了源账号与目标账号、转账的资金数额及确认号。直接将错误码与错误消息放到transferResponse 对象中会导致错误码与错误消息的定位变得困难。如果每个动作都将错误信息放到自己的响应对象中,就无法在应用间重用错误报告逻辑了。使用如下代码中的数据 包结构可以让应用快速确定是否出现了错误,方式是检查响应的JSON 负载中是否存在“error”对象:

  1. <span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">{"error":{ 
  2. "code":900005
  3. "messages":"Insufficient Funds to Complete Transfer" 
  4. }, 
  5. "data":{ 
  6. "fromAccount":1
  7. "toAccount":5
  8. "amount":500.00 
  9. }</span></span> 

报告错误的UI 代码是很容易重用的,因为错误信息总是位于响应负载的error 属性中。此外,实际的交易负载处理得到了简化,因为它总是位于相同的属性名之下。
无论请求失败的原因是什么,OS、HTTP 层还是应用,应用都必须能知道如何作出响应。你应该在开发时就提前考虑好应用所有的失败模式,并设计好一致的方式来检测并响应错误。
#p#

5.2 错误处理的经验法则
错误可能是由多种原因造成的,***处理方式也随编写的应用不同而不同。虽然很复杂,不过有一些经验法则可以帮助处理错误原因不可控的本质。
5.2.1 在接口契约中处理错误
在 设计服务接口时,只指定输入、输出与服务操作的做法是不正确的。接口契约还应该指定如何向客户端发送错误信息。服务接口应该使用业界标准方式在可能的情况 下传递错误信息。比如,服务器不应该为服务端失败定义新的HTTP 状态值;相反,应该使用恰当的500 级别的状态。如果使用了标准值,那么客户端与服务端开发者就能对如何传递错误信息达成共识。应用绝不应该依赖于非标准的状态或是其他属性值来确定错误出现 与否。
应用开发者也不应该依赖于当前服务器软件栈的行为来决定该如何处理错误。在部署了iOS 应用后,服务器软件栈可能会由于未来的升级或替换而改变行为。
5.2.2 错误状态可能不正确
移动网络有如下有别于传统Web 应用错误的不那么明显的行为:模糊不清的错误报告。从移动设备发往服务器的任何网络请求都有3 种可能的结果:
● 设备完全能够确认操作是成功的。比如,NSError 与HTTP 状态值都表明成功,返回的负载包含语义上正确的信息。
● 设备完全能够确认操作是失败的。比如,返回的应用负载包含来自于服务器的特定于本次操作的失败标识。 设备模糊地确认操作是失败的。比如,移动应用发出HTTP 请求以在两个账户间转账。请求被银行系统接收并正确地处理;然而,由于网络失败应答却丢失了,NSURLConnection 报告超时。超时发生了,但却是在转账请求成功之后发生的。如果重试该操作,那就会导致重复转账,可能还会造成账户透支。第3 种场景会导致应用出现意外和检测不到的错误行为。如果应用开发者不知道第3种场景的存在,那么他们可能就会错误地假设操作失败,然后不小心重试已经成功的 操作。知道整个操作失败还不够,开发者必须考虑导致请求失败的原因,以及自动重试每个失败的请求是否是恰当的。
5.2.3 验证负载
应用 开发者不应该认为如果没有OS 错误或HTTP 错误,负载就是有效的。在很多场景下,请求似乎是成功的,不过负载却是无效的。客户端与服务器之间传递的负载都一种验证机制。JSON 与XML 就是具备了验证机制的负载格式,不过以逗号分隔的值文件与HTML 就没有这种机制。

5.2.4 分离错误与正常的业务状况
服务契约不应该将正常的业务状况报告为错误。比如有个用户,由于可能的欺诈导致账户被锁定,锁定状态应该在数据负载中进行报告而不应该当作错误情况。分离错误与正常的业务状况会让代码保持恰当的关注分离。只有当出现问题时才应该将之看成错误。
5.2.5 总是检查HTTP 状态
总是检查HTTP 响应中的HTTP 状态,理解成功的状态值,甚至向相同的服务发出重复的调用也是如此。服务器的状态可能随时会发生变化,甚至在并行的调用间也是如此。
5.2.6 总是检查NSError 值
应用代码应该总是检查返回的NSError 值来确保OS 层没有出现问题。即便知道应用总是运行在信号良好的Wi-Fi 网络下也应该这样做。任何东西都有出错的可能性,代码在处理网络时也需要做好防御工作。
5.2.7 使用一致的方法来处理错误
网 络错误的产生原因是非常多的,很难一一列举出来,影响的多样性及范围也是非常大的。在设计应用时,请不要只关注于一致的用户界面模式或是一致的命名模式。 你还应该设计一致的模式来处理网络错误。该模式应该考虑到应用可能会遇到的所有类型的错误。如果应用的内部没有以一致的模式处理这些错误,那么应用就无法 以一致的方式向用户报告这些错误。
5.2.8 总是设置超时时间
在iOS 中,HTTP 请求的默认超时时间间隔是4 分钟,这对于移动应用来说过长了,大多数用户都不会在任何应用中等


iOS 简化了网络通信,不过对可能发生的所有类型的错误与边界条件作出响应则不是那么轻松的事情。常见的做法是在网络代码中放置钩子来快速查看结果,接下来再对 所有的错误情况进行处理。对于非移动应用来说,通常可以使用这种方式,因为来自工作站的网络连接是可预测的。如果在应用加载时有网络,那么当用户加载下一 个页面时基本上也会有网络。绝大多数情况都是这样的,开发者可以依赖浏览器向用户显示消息。如果在移动应用中没有及时添加异常处理,那么当后面遇到新的错 误源时就需要大幅重构网络代码。
#p#

5.3 优雅地处理网络错误

本节将会介绍一种设计模式,用来创建一个优雅且健壮的异常处理框架,并且在未来遇到新的错误时几乎不需要做什么工作就能很好地进行扩展。考虑如下3 个移动通信中的主要异常情况:
● 由于设备没有充分的网络连接导致远程服务器不可达。
● 由于OS 错误、HTTP 错误或是应用错误导致远程服务器返回错误响应。
● 服务器需要认证,而设备尝试发出未认证的请求。
随着可能的异常数量呈现出线性增长,处理这些异常的代码量则呈指数级增长。如果代码要在每一类请求中处理所有这些错误,那么代码的复杂性与数量就会呈指数级增长。本节将要介绍的模式会将这种指数级的曲线压成线性曲线。

5.3.2 指挥调度模式示例

本节通过调用YouTube 的一项认证服务来介绍指挥调度模式。在此类通信过程中需要考虑很多失败模式:
● 用户可能没有提供有效的身份信息。
● 设备可能无法联网。
● YouTube 可能没有及时响应或是出于某些原因失败了。
应用需要以一种优雅且可靠的方式处理每一种情况。该例将会阐述主要的代码组件并介绍一些实现细节。项目中的应用是个示例应用,只用于演示目的。
1. 前提条件
要想成功运行该应用,你需要准备好如下内容:
● 一个YouTube 账号。
● 至少向你的YouTube 账号上传一个视频(无须公开,只要上传到该账号即可)。
● 从Wrox 网站上下载的项目压缩文件。
该项目使用Xcode 4.1 与iOS 4.3 开发,应用使用的是截止到2011 年10 月份的YouTubeAPI,不过该API 处于Google 的控制下,而且可能会发生变化。
2. 主要对象
下载好项目并在Xcode 中加载后,你会看到如下类:
1) 命令
命令分组中有如下一些类。
BaseCommand
BaseCommand 是所有命令对象的父类。它提供了每个命令类所需的众多方法,这些方法有:
发送完成、错误与登录通知的方法。

用于让对象监听完成通知的方法。
用于支持实际的NSURLRequests 的方法。
BaseCommand 继承了NSOperation,因此所有的命令逻辑都位于该类的每个子类对象的main 方法中。
GetFeed
如 代码清单5-1 所示,该类的main 方法会调用YouTube 并加载当前登录用户上传的视频列表。YouTube 通过请求HTTP 头中的令牌来确定登录用户的身份。如果没有这个头,YouTube 就会返回HTTP 状态码0 而不是更加标准的4xx HTTP 错误。

代码清单5-1 CommandDispathDemo/service-interface/GetFeed.h

  1. <span style="font-family:Microsoft YaHei;font-size:14px;">- (NSMutableURLRequest *) createRequestObject:(NSURL *)url { 
  2. NSMutableURLRequest *request = [[[NSMutableURLRequestalloc] 
  3. initWithURL:url 
  4. cachePolicy:NSURLCacheStorageAllowed 
  5. timeoutInterval:20 
  6. autorelease]; 
  7. return request; 
  8. }</span> 

在 上述代码清单中,通过self 调用的很多方法都是在BaseCommand 父类中实现的。GetFeed 命令就是指挥调度模式的原型。main 方法会对用户登录进行检查,因为如果这个调用失败了,那就没必要再调用服务器了。如果用户已经登录,那么代码就会构建请求,将认证头添加到请求中,然后发 送一条同步请求。代码的***一部分会调用一个父类方法来确定调用是否成功。该方法使用来自于NSHTTPURLResponse 对象的NSError 对象与HTTP 状态码来确定是否成功。如果调用失败,就会广播一条错误通知或是需要登录的通知。
LoginCommand
该命令会向YouTube 发出对用户进行认证的请求。该命令比较独立,因为并没有使用BaseCommand 对象的辅助方法。之所以没有使用这些方法,是因为如果登录失败,就不应该生成需要认证的失败消息,而只会报告正常完成或是失败的状态。
登 录监听器会处理来自于登录失败的错误。要想了解关于YouTube 所需协议的详细信息,请参考 http://code.google.com/apis/youtube/2.0 /developers_guide_protocol_understanding_video_feeds.html。
2) 异常监听器
监 听器分组中有视图控制器, 当错误发生或是用户需要登录时会呈现出来。NetworkErrorViewController 与LoginViewController 都继承了InterstitialViewController,后者提供了几个常用的辅助方法。这两个视图控制器都会以模态视图控制器的形式呈现出来。
● NetworkErrorViewController:向用户提供重试或是放弃失败操作的选择。如果用户选择重试,那么失败命令就会放回到操作队列中。
● LoginViewController:向用户请求用户名与密码。位于视图栈的顶部,直到用户成功登录为止。 InterstitialViewController:作为其他异常监听器的父监听器,提供了一些支持功能,比如收集多个错误通知以及当错误解析完毕时 重新分发错误的代码等。监听器的关键代码位于viewDidDisappear:方法中(如代码清单5-2 所示),当视图完全消失时会调用该方法。如果在视图完全消失前命令已进入队列中,那么其他错误就有可能导致再一次呈现视图,这会导致应用出现严重的错误。 iOS 5 提供了处理这个问题的更好方式,因为在视图消失时用户可以指定执行的代码块。在处理触发命令前,代码并不需要确定消失的原因。

代码清单5-2 CommandDispatchDemo/NetworkErrorViewController.m

  1. <span style="font-family:Microsoft YaHei;font-size:14px;">- (void) viewDidDisappear:(BOOL)animated { 
  2. if(retryFlag) { 
  3. // re-enqueue all of the failed commands 
  4. [self performSelectorAndClear:@selector(enqueueOperation)]; 
  5. else { 
  6. // just send a failure notification for all failed commands 
  7. [self performSelectorAndClear: 
  8. @selector(sendCompletionFailureNotification)]; 
  9. self.displayed = NO; 
  10. }</span> 

应用委托会将自身注册为网络错误与需要登录通知的监听器(如代码清单5-3 所示),收集异常通知并在错误发生时管理正确的视图控制器的呈现。上述代码展示了需要登录通知的通知处理器。由于要处理用户界面,因此其中的内容必须使用GCD 在主线程中执行。
代码清单5-3 CommandDispatchDemo/CommandDispatchDemoAppDelegate.m

  1. <span style="font-family:Microsoft YaHei;font-size:14px;">/** 
  2. * Handles login needed notifications generated by commands 
  3. **/ 
  4. - (void) loginNeeded:(NSNotification *)notif { 
  5. // make sure it all occurs on the main thread 
  6. dispatch_async(dispatch_get_main_queue(), ^{ 
  7. // make sure only one thread adds a command at a time 
  8. @synchronized(loginViewController) { 
  9. [loginViewController addTriggeringCommand: 
  10. [notif object]; 
  11. if(!loginViewController.displayed) { 
  12. // if the view is not displayed then display it. 
  13. [[self topOfModalStack:self.window.rootViewController] 
  14. presentModalViewController:loginViewController 
  15. animated:YES]; 
  16. loginViewController.displayed = YES; 
  17. }); // End of GC Dispatch block 
  18. }</span> 

3) 视图控制器
在 这个简单的应用中有个主要的视图控制器。RootViewController(参见下面的代码)继承了UITableViewController。当 该控制器加载时,会创建并排队命令以加载用户的视频列表(又叫做YouTube 种子),并且会将控制流放回到主运行循环中以耐心等待命令的完成。
***次调用总是失败的,因为这时用户还没有登录。CommandDispatchDemo/RootViewController.m 的requestVideoFeed 方法会启动加载视频列表的过程,如下所示:

  1. <span style="font-family:Microsoft YaHei;font-size:14px;">(void)requestVideoFeed { 
  2. // create the command 
  3. GetFeed *op = [[GetFeedalloc] init]; 
  4. // add the current authentication token to the command 
  5. CommandDispatchDemoAppDelegate *delegate = 
  6. (CommandDispatchDemoAppDelegate *)[[UIApplication 
  7. sharedApplication] delegate ]; 
  8. op.token = delegate.token; 
  9. // register to hear the completion of the command</span> 
  10.  
  11. <span style="font-family:Microsoft YaHei;font-size:14px;"><span style="background-color: rgb(255, 255, 255);">[op listenForMyCompletion:self selector:@selector(gotFeed:)];</span></span> 
  12.  
  13. <span style="font-family:Microsoft YaHei;font-size:14px;">// put it on the queue for execution 
  14. [op enqueueOperation]; 
  15. [op release]; 
  16. }</span> 

注意,代码并不需要检查用户是否已经登录;在执行时命令会做检查。
gotFeed:方法会处理来自于YouTube 的最终返回数据。在此例中,requestVideoFeed:方法会将gotFeed:方法注册为完成通知的目标方法。如果调用成功,该方法会将数据加载
到表视图中,否则显示UIAlertView:

  1. <span style="font-family:Microsoft YaHei;font-size:14px;">- (void) gotFeed:(NSNotification *)notif { 
  2. NSLog(@"User info = %@", notif.userInfo); 
  3. BaseCommand *op = notif.object; 
  4. if(op.status == kSuccess) { 
  5. self.feed = op.results; 
  6. // if entry is a single item, change it to an array, 
  7. // the XML reader cannot distinguish single entries 
  8. // from arrays with only one element 
  9. id entries = [[feed objectForKey:@"feed"] objectForKey:@"entry"]; 
  10. if([entries isKindOfClass:[NSDictionary class]]) { 
  11. NSArray *entryArray = [NSArrayarrayWithObject:entries]; 
  12. [[feed objectForKey:@"feed"] setObject:entryArrayforKey:@"entry"]; 
  13. dispatch_async(dispatch_get_main_queue(), ^{ 
  14. [self.tableViewreloadData]; 
  15. }); 
  16. else { 
  17. dispatch_async(dispatch_get_main_queue(), ^{ 
  18. UIAlertView *alert = [[UIAlertViewalloc] 
  19. initWithTitle:@"No Videos" 
  20. message:@"The login to YouTube failed" 
  21. delegate:self 
  22. cancelButtonTitle:@"Retry" 
  23. otherButtonTitles:nil]; 
  24. [alert show]; 
  25. [alert release]; 
  26. }); 
  27. YouTubeVideoCell 是UITableViewCell 的子类,它会异步加载视频的缩略图。它通过LoadImageCommand 对象完成加载处理: 
  28. /** 
  29. * Start the process of loading the image via the command queue 
  30. **/ 
  31. - (void) startImageLoad { 
  32. LoadImageCommand *cmd = [[LoadImageCommandalloc] init]; 
  33. cmd.imageUrl = imageUrl; 
  34. // set the name to something unique 
  35. cmd.completionNotificationName = imageUrl; 
  36. [cmd listenForMyCompletion:self selector:@selector(didReceiveImage:)]; 
  37. [cmdenqueueOperation]; 
  38. [cmd release]; 
  39. }</span> 

这个类会改变完成通知名,这样它(也只有它)就可以接收到特定图片的通知了。否则,它还需要检查返回的通知来确定是否是之前发出的命令。
指 挥调度模式的优雅之处在于能将应用中所有凌乱的异常处理逻辑和登录呈现逻辑与主视图控制器分离开来。当视图控制器发出命令时,会忽略掉所有的异常处理与认 证处理,只是完成请求而已。只是发出请求,等待响应,然后处理响应。并不关心用户注册的请求是不是重试了5 次才成功。此外,服务请求代码并不需要知道请求来自于哪里,结果去向哪里;只是关注于执行调用并广播结果。

指挥调度模式还有其他优势,开发者一开始会编写一些代码并论证结果,如果顺利,那么会添加异常处理器,而这对之前的代码不会造成任何影响。此外,如果设计恰当,那么所有的网络服务调用都会使用相同的基础命令类,这会减少命令类的数量。

在通用应用中,可以通过异常监听器调整展示的视图,这样iPhone 上的错误显示界面就会适配于该平台,iPad 上的错误显示界面也会适配于更大的平台。

这种模式可以快速展示结果,对业务逻辑与异常处理进行关注分离,减少重复代码以及提供更好的用户体验。

5.4 小结
代码使用网络时会出现很多错误源,理解错误源有助于快速诊断并解析网络问题。借助于Reachability 框架,代码

本文链接:http://my.oschina.net/louiseliu/blog/289253

责任编辑:chenqingxiang 来源: oschina
相关推荐

2021-04-14 07:08:14

Nodejs错误处理

2011-06-28 09:28:32

InformationiPadiPhone

2017-04-06 14:40:29

JavaScript错误处理堆栈追踪

2017-03-08 08:57:04

JavaScript错误堆栈

2024-03-27 08:18:02

Spring映射HTML

2021-04-29 09:02:44

语言Go 处理

2014-11-17 10:05:12

Go语言

2011-08-10 10:10:21

iPhoneUIPopoverCo

2023-11-08 15:04:55

事务GORM

2011-07-06 15:04:41

ASP

2022-05-06 08:00:51

Golang编程语言Java

2023-10-26 15:49:53

Go日志

2023-12-26 22:05:53

并发代码goroutines

2011-08-11 10:03:43

iPhonecocoaNSRunLoop

2009-08-05 16:04:50

2023-10-28 16:30:19

Golang开发

2010-06-01 16:14:04

2022-11-16 08:41:43

2011-08-04 15:24:53

iPhone开发 交互 MVC

2014-07-16 10:08:30

点赞
收藏

51CTO技术栈公众号