iOS网络高级编程:iPhone和iPad的企业应用开发之错误处理
本章内容
● iOS应用中的网络错误源
● 检测网络的可达性
● 错误处理的经验法则
● 处理网络错误的设计模式
到目前为止,我们所介绍的iPhone与其他系统的网络交互都是基于一切正常这个假设。本章将会放弃这个假设,并深入探究网络的真实世界。在真实世界中,事情是会出错的,有时可能是非常严重的错误:手机进入与离开网络、包丢掉或是延迟;网络基础设施出错;偶尔用户还会出错。如果一切正常,那么编写iOS应用就会简单不少,不过遗憾的是现实并非如此。本章将会探讨导致网络操作失败的几个因素,介绍系统如何将失败情况告知应用,应用又该如何优雅地通知用户。此外,本章还将介绍如何在不往应用逻辑中添加错误处理代码的情况下,以一种整洁且一致的方式处理错误的软件模式。
5.1 理解错误源
早期的iOS有个很棒的天气预报应用。它在Wi-Fi和信号良好的蜂窝网络下使用正常,不过当网络质量不那么好时,这个天气预报应用就像感冒似的,在主屏幕上崩溃。有不少应用在出现网络错误时表现很差劲,会疯狂弹出大量UIAlertView以告诉用户出现了“404 Error on Server X”等类似信息。还有很多应用在网络变慢时界面会变得没有响应。这些情况的出现都是没有很好地理解网络失败模式以及没有预期到可能的网络降级或是失败。如果想要避免这类错误并能够充分地处理网络错误,那么你首先需要理解它们的起源。
考虑一个字节是如何从设备发往远程服务器以及如何从远程服务器将这个字节接收到设备,这个过程只需要几百毫秒的时间,不过却要求网络设备都能正常工作才行。设备网络与网络互联的复杂性导致了分层网络的产生。分层网络将这种复杂环境划分成了更加易于管理的模块。虽然这对程序员很有帮助,不过当数据在各个层之间流动时可能会产生之前提到的网络错误。图5-1展示了Internet协议栈的各个层次。
图5-1
每一层都会执行某种错误检测,这可能是数学意义上的、逻辑意义上的,或是其他类型的检测。比如,当网络接口层接收到某一帧时,它首先会通过错误校正码来验证内容,如果不匹配,那么错误就产生了。如果这个帧根本就没有到达,那就会产生超时或是连接重置。错误检测出现在栈的每一层,自下而上直到应用层,应用层则会从语法和语义上检查消息。
在使用iOS中的URL加载系统时,虽然手机与服务器之间的连接可能会出现各种各样的问题,不过可以将这些原因分成3种错误类别,分别是操作系统错误、HTTP错误与应用错误。这些错误类别与创建HTTP请求的操作序列相关。图5-2展示了向应用服务器发出的HTTP请求(提供来自于企业网络的一些数据)的简单序列图。每块阴影区域都表示这3种错误类型的错误域。典型地,操作系统错误是由HTTP服务器问题导致的。HTTP错误是由HTTP服务器或应用服务器导致的。应用错误是由请求传输的数据或应用服务器查询的其他系统导致的。
图5-2
如果请求是安全的HTTPS请求,或是HTTP服务器被重定向客户端,那么上面这个序列的步骤将会变得更加复杂。上述很多步骤都包含着大量的子步骤,比如在建立TCP连接时涉及的SYN与SYN-ACK包序列等。下面将会详细介绍每一种错误类别。
5.1.1 操作系统错误
操作系统错误是由数据包没有到达预定目标导致的。数据包可能是建立连接的一部分,也可能位于连接建立的中间阶段。OS错误可能由如下原因造成:
● 没有网络——如果设备没有数据网络连接,那么连接尝试很快就会被拒绝或是失败。这些类型的错误可以通过Apple提供的Reachability框架检测到,本节后面将会对此进行介绍。
● 无法路由到目标主机——设备可能有网络连接,不过连接的目标可能位于隔离的网络中或是处于离线状态。这些错误有时可以由操作系统迅速检测到,不过也有可能导致连接超时。
● 没有应用监听目标端口——在请求到达目标主机后,数据包会被发送到请求指定的端口号。如果没有服务器监听这个端口或是有太多的连接请求在排队,那么连接请求就会被拒绝。
● 无法解析目标主机名——如果无法解析目标主机名,那么URL加载系统就会返回错误。通常情况下,这些错误是由配置错误或是尝试访问没有外部名字解析且处于隔离网络中的主机造成的。
在iOS的URL加载系统中,操作系统错误会以NSError对象的形式发送给应用。iOS通过NSError在软件组件间传递错误信息。相比简单的错误代码来说,使用NSError的主要优势在于NSError对象包含了错误域属性。
不过,NSError对象的使用并不限于操作系统。应用可以创建自己的NSError对象,使用它们在应用内传递错误消息。如下代码片段展示的应用方法使用NSError向调用的视图控制器传递回失败信息:
-(id)fetchMyStuff:(NSURL*)url error:(NSError**)error
{
BOOL errorOccurred = NO;
// some code that makes a call and may fail
if(errorOccurred) //some kind of error
{
NSMutableDictionary *errorDict = [NSMutableDictionary dictionary];
[errorDictsetValue:@"Failed to fetch my stuff"
forKey:NSLocalizedDescriptionKey];
*error = [NSErrorerrorWithDomain:@"myDomain"
code:kSomeErrorCode
userInfo:errorDict];
return nil;
} else {
return stuff
}
}
Nach dem Login kopieren
域属性根据产生错误代码的库或框架对这些错误代码进行隔离。借助域,框架开发者无须担心覆盖错误代码,因为域属性定义了产生错误的框架。比如,框架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 请求的代码示例:
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">NSHTTPURLResponse *response=nil;
NSError *error=nil;
NSData *myData=[NSURLConnectionsendSynchronousRequest:request
returningResponse:&response
error:&error];
if (!error) {
// No OS Errors, keep going in the process
...
} else {
// Something low level broke
}</span></span>
Nach dem Login kopieren
注意,NSError 对象被声明为指向nil 的指针。如果出现错误,那么NSURLConnection对象只会实例化NSError 对象。URL 加载系统拥有NSError 对象;如果稍后代码会用到它,那么应该保持这个对象。如果在同步请求完成后NSError 指针依然指向nil,那就说明没有产生底层的OS 错误。这时,代码就知道没有产生OS 级别的错误,不过错误可能出现在协议栈的某个高层。
如果应用创建的是异步请求,那么NSError 对象就会返回到委托类的下面这个方法:
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">- (void)connection:(NSURLConnection *)connection
didFailWithError:(NSError *)error</span></span>
Nach dem Login kopieren
这是传递给请求委托的最终消息,委托必须能识别出错误的原因并作出恰当的反应。在如下示例中,委托会向用户展UIAlertView:
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">- (void) connection:conndidFailWithError:error {
UIAlertView *alert = [UIAlertViewalloc] initWithTitle:@"Network Error"
message:[error description]
delegate:self
cancelButtonTitle:@"Oh Well"
otherButtonTitles:nil];
[alert show];
[alert release];
}</span></span>
Nach dem Login kopieren
上述代码以一种生硬且不友好的方式将错误展现给了用户。在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 地址是可达的,只是标识是否存在网络连接。
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">#import "Reachability.h"
...
if([[Reachability reachabilityForInternetConnection]
currentReachabilityStatus] == NotReachable) {
// handle the lack of a network
}</span></span>
Nach dem Login kopieren
在某些情况下,你可能想要修改某些动作、禁用UI 元素或是当设备处于有限制的网络中时修改超时值。如果应用需要知道当前正在使用的连接类型,那么请使用如下代码:
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">#import "Reachability.h"
...
NetworkStatus reach = [[Reachability reachabilityForInternetConnection]
currentReachabilityStatus];
if(reach == ReachableViaWWAN) {
// Network Is reachable via WWAN (aka. carrier network)
} else if(reach == ReachableViaWiFi) {
// Network is reachable via WiFi
}</span></span>
Nach dem Login kopieren
知道设备可达性状态的变化也是很有必要的,这样就可以主动修改应用行为。如下代码片段启动对网络状态的监控:
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">#import "Reachability.h"
...
[[NSNotificationCenterdefaultCenter]
addObserver:self
selector:@selector(networkChanged:)
name:kReachabilityChangedNotification
object:nil];
Reachability *reachability;
reachability = [[Reachability reachabilityForInternetConnection] retain];
[reachability startNotifier];</span></span>
Nach dem Login kopieren
上述代码将当前对象注册为通知观察者,名为kReachabilityChangedNotification。
NSNotificationCenter 会调用当前对象的名为networkChanged:的方法。当可达性状态发生变化时,就向该对象传递NSNotification 及新的可达性状态。如下示例展示了通知监听者:
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">- (void) networkChanged: (NSNotification* )notification
{
Reachability* reachability = [notification object];
第Ⅱ部分 HTTP 请求:iOS 网络功能
98
if(reachability == ReachableViaWWAN) {
// Network Is reachable via WWAN (a.k.a. carrier network)
} else if(reachability == ReachableViaWiFi) {
// Network is reachable via WiFi
} else if(reachability == NotReachable) {
// No Network available
}
}</span></span>
Nach dem Login kopieren
可达性还可以确定当前网络上某个特定的主机是否是可达的。可以通过该特性根据应用是处于内部隔离的网络上还是公开的Internet 上调整企业应用的行为。如下代码示例展示了该特性:
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">Reachability *reach = [Reachability
reachabilityWithHostName:@"www.captechconsulting.com"];
if(reachability == NotReachable) {
// The target host is not reachable available
}</span></span>
Nach dem Login kopieren
请记住,该特性对目标主机的访问有个来回。如果每个请求都使用该特性,那就会极大增加应用的网络负载与延迟。Apple 建议不要在主线程上检测主机的可达性,因为尝试访问主机可能会阻塞主线程,这会导致UI 被冻结。
OS 错误首先就表明请求出现了问题。应用开发者有时会忽略掉它们,不过这样做是有风险的。因为HTTP 使用了分层网络,这时HTTP 层或是应用层可能会出现其他类型的潜在失败情况。
5.1.2 HTTP 错误
HTTP 错误是由HTTP 请求、HTTP 服务器或应用服务器的问题造成的。HTTP 错误通过HTTP 响应的状态码发送给请求客户端。
404 状态是常见的一种HTTP 错误,表示找不到URL 指定的资源。下述代码片段中的HTTP 头就是当HTTP 服务器找不到请求资源时给出的原始输出:
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">HTTP/1.1 404 Not Found
Date: Sat, 04 Feb 2012 18:32:25 GMT
Server: Apache/2.2.14 (Ubuntu)
Vary: Accept-Encoding
Content-Encoding: gzip
Content-Length: 248
Keep-Alive: timeout=15, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=iso-8859-1</span></span>
Nach dem Login kopieren
响应的第一行有状态码。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 服务器返回的成功状态的验证:
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">NSHTTPURLResponse *response=nil;
NSError *error=nil;
NSData *myData = [NSURLConnectionsendSynchronousRequest:request
returningResponse:&response
error:&error];
//Check the return
if((!error) && ([response statusCode] == 200)) {
// looks like things worked
} else {
// things broke, again.
}</span></span>
Nach dem Login kopieren
如果请求的URL不是HTTP,那么应用就应该验证响应对象是否是NSHTTPURLResponse对象。验证对象类型的首选方法是使用返回对象的isKindOfClass:方法,如下所示:
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">if([response isKindOfClass:[NSHTTPURLResponse class]]) {
// It is a HTTP response, so we can check the status code
...</span></span>
Nach dem Login kopieren
要想了解关于HTTP 状态码的权威信息,请参考W3 RFC 2616,网址是http://www.w3.org/Protocols/rfc2616/rfc2616.html。
5.1.3 应用错误
本节将会介绍网络协议栈的下一层(应用层)产生的错误。应用错误不同于OS 错误或HTTP 错误,因为并没有针对这些错误的标准值或是原因的集合。这些错误是由运行在服
务层之上的业务逻辑和应用造成的。在某些情况下,错误可能是代码问题,比如异常,不过在其他一些情况下,错误可能是语义错误,比如向服务提供了无效的账号等。对于前者来说,建议生成HTTP 500 级别的错误;对于后者来说,应该在应用负载中返回错误码。
比如,如果用户尝试从账户中转账的金额超出了账户的可用余额,那么手机银行就应该报告应用错误。如果发出了这样的请求,那么OS 会说请求成功发送并接收到了响应。HTTP 服务器会报告接收到了请求并发出了响应,不过应用层必须报告这笔交易失败。报告应用错误的最佳实践是将应用的负载数据封装在标准信封中,信封中含有一致的应用错误位置信息。在上述资金转账示例中,成功的转账响应的业务负载应该如下所示:
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">{ "transferResponse":{
"fromAccount":1,
"toAccount":5,
"amount":500.00,
"confirmation":232348844
}
}</span></span>
Nach dem Login kopieren
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">
</span></span>
Nach dem Login kopieren
响应包含了源账号与目标账号、转账的资金数额及确认号。直接将错误码与错误消息放到transferResponse 对象中会导致错误码与错误消息的定位变得困难。如果每个动作都将错误信息放到自己的响应对象中,就无法在应用间重用错误报告逻辑了。使用如下代码中的数据包结构可以让应用快速确定是否出现了错误,方式是检查响应的JSON
负载中是否存在“error”对象:
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">{"error":{
"code":900005,
"messages":"Insufficient Funds to Complete Transfer"
},
"data":{
"fromAccount":1,
"toAccount":5,
"amount":500.00
}
}</span></span>
Nach dem Login kopieren
报告错误的UI 代码是很容易重用的,因为错误信息总是位于响应负载的error 属性中。此外,实际的交易负载处理得到了简化,因为它总是位于相同的属性名之下。
无论请求失败的原因是什么,OS、HTTP 层还是应用,应用都必须能知道如何作出响应。你应该在开发时就提前考虑好应用所有的失败模式,并设计好一致的方式来检测并响应错误。
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 分钟,这对于移动应用来说过长了,大多数用户都不会在任何应用中等待4 分钟。开发者需要选择合理的超时时间,方式是评估网络请求的可能响应时间,然后将最差的网络场景下的网络延迟考虑进去。如下示例展示了如何创建具有20 秒超时时间的请求:
<span style="font-family:Microsoft YaHei;font-size:14px;">- (NSMutableURLRequest *) createRequestObject:(NSURL *)url {
NSMutableURLRequest *request = [[[NSMutableURLRequestalloc]
initWithURL:url
cachePolicy:NSURLCacheStorageAllowed
timeoutInterval:20
autorelease];
return request;
}</span>
Nach dem Login kopieren
5.3 优雅地处理网络错误
iOS 简化了网络通信,不过对可能发生的所有类型的错误与边界条件作出响应则不是那么轻松的事情。常见的做法是在网络代码中放置钩子来快速查看结果,接下来再对所有的错误情况进行处理。对于非移动应用来说,通常可以使用这种方式,因为来自工作站的网络连接是可预测的。如果在应用加载时有网络,那么当用户加载下一个页面时基本上也会有网络。绝大多数情况都是这样的,开发者可以依赖浏览器向用户显示消息。如果在移动应用中没有及时添加异常处理,那么当后面遇到新的错误源时就需要大幅重构网络代码。
本节将会介绍一种设计模式,用来创建一个优雅且健壮的异常处理框架,并且在未来遇到新的错误时几乎不需要做什么工作就能很好地进行扩展。考虑如下3 个移动通信中的主要异常情况:
● 由于设备没有充分的网络连接导致远程服务器不可达。
● 由于OS 错误、HTTP 错误或是应用错误导致远程服务器返回错误响应。
● 服务器需要认证,而设备尝试发出未认证的请求。
随着可能的异常数量呈现出线性增长,处理这些异常的代码量则呈指数级增长。如果代码要在每一类请求中处理所有这些错误,那么代码的复杂性与数量就会呈指数级增长。本节将要介绍的模式会将这种指数级的曲线压成线性曲线。
5.3.1 设计模式介绍
本节介绍的模式联合使用了指挥调度模式与广播通知。该模式包含如下类型的对象:
● 控制器
● 命令对象
● 异常监听器
● 命令队列
下面从高层次来介绍每一类对象的行为。
1. 对象说明
下面介绍构成指挥调度模式的对象的特性及属性。
控制器
控制器通常指的是视图控制器,用来请求数据并处理结果。在该设计模式中,控制器无须包含任何异常处理逻辑。唯一需要控制器处理的错误情况就是成功完成或是完全不可恢复的服务失败。在不可恢复失败这个场景中,控制器通常会将自己从视图栈中弹出,因为用户这时已经收到接下来要介绍的异常监听器对象发出的失败通知了。控制器会创建命令并监听命令的完成情况。
命令对象
命令对象与应用执行的不同网络交易相关。检索图片、从指定的REST 端点处获取JSON 数据或是向服务发出POST 信息等都是命令对象请求。命令对象是NSOperation 的子类。由于命令对象中的大多数逻辑都与其他类型的命令对象相同,因此可以创建父类命令象来处理,让特定的命令继承该逻辑。命令对象具有如下属性:
● 完成通知名——在iOS 中,控制器会将自身注册为该通知名的观察者。当服务调用成功返回时,命令对象会通过NSNotificationCenter 使用该名字来广播通知。虽然该名字对于命令类来说通常是唯一的,不过在某些情况下,如果有多个控制器发出相同类型的命令(区分不同的响应),那么这个名字针对于特定的实例将是唯一的。
● 服务器错误异常通知名——特定的异常处理器对象会监听该通知。当服务器超时、返回与认证相关的OS 错误或HTTP 错误时,命令对象会通过NSNotificationCenter并使用该名字来广播消息。通常情况下,所有的命令类会共享相同的异常名,因此也会共享相同的异常监听器。不过不同的命令类可能需要使用不同的异常监听器,并且有不同的服务器错误异常名。
● 可达性异常通知名——当检测到无法到达Internet 或目标主机时,命令对象会生成该类型的通知。另一个异常监听器可以监听该类型的异常。在某些应用中,这类常是不需要的,因为服务器错误异常监听器会处理可达性异常。
● 认证异常通知名——如果确定用户没有认证或是服务器报告了未认证状态,那么命令对象可能会产生该类型的通知。第3 个异常监听器会等待该类型通知的出现。认证通知名通常会在应用的所有通知中共享。
● 自定义属性——这些属性特定于发出的请求。控制器通常会提供这些值,因为它们特定于服务调用所需的业务数据,而且不同的调用数据也是不同的。异常监听器
一般来说,每个异常监听器都是由应用委托实例化的,位于后台并等待着特定类型的通知。在很多情况下,异常监听器在接收到通知时会显示模态视图控制器,这将在“异常监听器行为”部分进行介绍。
命令队列
控制器会将命令提交到命令队列进行处理,应用可能有一个或多个命令队列。在iOS中,命令队列是NSOperationQueue 的子类。不应该将主队列用作命令队列,因为它的操作运行在用户界面线程上, 在执行长时间运行的操作时会影响到用户体验。
NSOperationQueues 提供了管理活动操作以及操作间依赖的内置功能。
2. 对象上述每一个对象都在成功完成网络交易的过程中扮演着各自不同的角色。下面介绍它们在该模式中各自的角色。
控制器行为
控制器重点关注于执行UI 与业务逻辑。当控制器想要从服务获取数据时,应该采取如下动作:
(1) 创建一个网络命令对象。
(2) 针对命令对象的具体属性初始化请求。
(3) 注册为命令完成的观察者。
(4) 将命令推送到操作队列中准备执行。
(5) 等待NSNotificationCenter 发送完成通知。
当操作完成时,控制器会接收到完成通知并采取如下动作:
(1) 检查操作状态,看看操作是否成功。
(2) 如果成功,那么控制器会处理接收到的数据。接收到的数据是通过NSNotification对象的userInfo 属性提供给控制器的。NSOperationQueues 会在自己的线程上执行NSOperation 对象。当操作完成后,会通过NSNotificationCenter 发送NSNotification。该通知回调方法会在NSOperation 运行的线程上得到调用,在该例中这会确保它不会进入主线程中。如果控制器操纵UI,那么需要在主线程上做这些改变,通常是通过Grand CentralDispatch(GCD)实现的。
(3) 如果不成功,那么控制器根据应用需求可以有很多选择。比如,可以将自己从视图栈中弹出或是更新UI,表明数据不可用。控制器不应该重试或是显示模态警告,因为这
些动作是异常监听器的职责。
(4) 控制器应该将自身从命令完成通知的观察者中移除。在某些情况下,如果控制器想要监控来自于相同类型命令的其他数据,那就没必要这么做了。注意,控制器不包含处理重试、超时、认证或可达性的任何逻辑;这些逻辑都是由命令与异常监听器实现的。
如果控制器想要确保只有它才能接收到返回的数据,那么就应该在将其放到队列中之前改变通知名,将其改为针对该命令对象实例唯一的值,然后监听这个名字的通知。命令对象行为命令对象负责调用目标服务并将服务调用的结果广播出去。一般来说,命令对象需要执行如下步骤:
(1) 检查可达性。如果网络不可达,那就广播一条可达性异常通知。
(2) 如果需要,检查认证状态。如果用户尚未授权,那就广播一条可达性异常通知。
(3) 使用控制器提供的自定义属性构建网络请求。通常情况下,端点URL 是命令对象类的静态属性或是从配置子系统中加载。
(4) 使用同步请求方式发出网络请求。参见第3 章的3.3.2 节“同步请求”以了解详情。
(5) 检查请求状态。如果状态是OS 错误或HTTP 错误,那就广播一条服务器异常通知。如果是认证错误,那就广播一条认证异常通知。
(6) 解析结果,参见第4 章。
(7) 广播一条成功状态的完成通知。
当命令对象广播通知时,无论是成功还是其他通知,都需要创建字典对象,字典对象中包含自身的副本、调用状态与返回的数据,以此作为调用的结果。自身复制是有必要的,因为NSOperation 实例只能执行一次。稍后将会介绍,在监听器处理异常时,命令可能会被再次提交。
同步请求API 非常适合于该模式,因为命令是在后台线程而非主线程中执行的。如果请求发出或是返回的数据量超出期望在内存中处理的数据量,应用就需要使用异步请求了。因为NSOperation 的主要功能是一个方法,操作必须实现并发锁来阻塞main
方法,直到异步调用完成为止。
异常监听器行为
这种模式之所以如此强大,异常监听器功不可没。这些对象通常都是由应用委托创建的,驻留在内存中并监听着通知。当接收到通知时,监听器会通知用户,还可能会接收来自用户的响应。当异常发生时,通知中包含了触发该异常的命令副本,当用户作出响应后,监听器通常会再次将命令发回到队列中并重试。关于异常监听器有趣的一点是:由于多个命令可能同时发生,因此在用户响应第一个异常时可能还会同时产生多个异常通知。出于这一点,异常监听器必须收集异常通知,然后在用户响应完第一个异常后重新提交所有的触发命令。这个错误集合可以避免一种常见的应用行为不当——用户被相同问题触发的多个UIAlertView
连续轰炸。
服务器异常的流程如下所示:
(1) 呈现一个漂亮的模态对话框,列出错误信息并让用户选择取消或是重试。
(2) 收集可能被广播的其他服务器异常。
(3) 如果用户选择重试,那么关闭对话框并重新提交所有收集到的命令。
(4) 如果用户选择取消,那么关闭对话框。监听器应该将所有收集到的命令的完成状态设为失败,然后让每个命令广播一条完成通知。
可达性异常的流程如下所示:
(1) 呈现一个漂亮的模态对话框,通知用户需要网络连接。
(2) 收集可能会被广播的其他服务异常。
(3) 监听可达性变更。当网络可达时,关闭对话框并重新提交收集到的命令。认证异常的流程稍微有点复杂。请记住,命令之间是独立的,在任意时刻可能会有多个命令同时发生。认证流程并不会生成认证异常通知,流程如下所示:
(1) 呈现一个模态登录视图。
(2) 继续收集由于认证错误导致的失败命令。
(3) 如果用户取消,那么监听器应该针对收集到的命令使用失败状态发送一条完成通知。
(4) 如果用户提供了认证信息,那么创建一个登录命令,将其放到命令队列中。
(5) 等待登录命令的完成通知。
(6) 如果由于用户名/密码不匹配而导致登录失败,那么回到步骤(2),否则关闭登录视图控制器。
(7) 如果登录命令成功,那么重新向命令队列提交触发命令。
(8) 如果登录命令失败,那么让触发命令使用失败状态发送一条完成通知。
命令队列行为
命令队列是原生的iOSNSOperationQueue 对象。在默认情况下,命令队列遵循着先进
先出(FIFO)的顺序。在代码向NSOperationQueue 中添加命令对象后,执行如下动作:
(1) 保持命令对象,这样其内存就不会被释放掉。
(2) 等待,直到队列头有可用位置。
(3) 当命令对象到达队列头时,命令对象的start 方法会被调用。
(4) 命令对象的main 方法得到调用。
请参考iOS API 文档中关于NSOperation 与NSOperationQueue 对象的介绍来了解队列与命令对象之间交互的详细信息。
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
<span style="font-family:Microsoft YaHei;font-size:14px;">- (void)main {
NSLog(@"Starting getFeed operation");
// Check to see if the user is logged in
if([self isUserLoggedIn]) { // only do this if the user is logged in
// Build the request
NSString *urlStr =
@"https://gdata.youtube.com/feeds/api/users/default/uploads";
NSLog(@"urlStr=%@",urlStr);
NSMutableURLRequest *request =
[ self createRequestObject:[NSURL URLWithString:urlStr]];
// Sign the request with the user’s auth token
[self signRequest:request];
// Send the request
NSHTTPURLResponse *response=nil;
NSError *error=nil;
NSData *myData = [self sendSynchronousRequest:request
response_p:&response
error:&error];
// Check to see if the request was successful
if([super wasCallSuccessful:responseerror:error]) {
[self buildDictionaryAndSendCompletionNotif: myData];
}
}
}</span>
Nach dem Login kopieren
在上述代码清单中,通过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
<span style="font-family:Microsoft YaHei;font-size:14px;">- (void) viewDidDisappear:(BOOL)animated {
if(retryFlag) {
// re-enqueue all of the failed commands
[self performSelectorAndClear:@selector(enqueueOperation)];
} else {
// just send a failure notification for all failed commands
[self performSelectorAndClear:
@selector(sendCompletionFailureNotification)];
}
self.displayed = NO;
}</span>
Nach dem Login kopieren
应用委托会将自身注册为网络错误与需要登录通知的监听器(如代码清单5-3
所示),收集异常通知并在错误发生时管理正确的视图控制器的呈现。上述代码展示了需要登录通知的通知处理器。由于要处理用户界面,因此其中的内容必须使用GCD 在主线程中执行。
代码清单5-3 CommandDispatchDemo/CommandDispatchDemoAppDelegate.m
<span style="font-family:Microsoft YaHei;font-size:14px;">/**
* Handles login needed notifications generated by commands
**/
- (void) loginNeeded:(NSNotification *)notif {
// make sure it all occurs on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
// make sure only one thread adds a command at a time
@synchronized(loginViewController) {
[loginViewController addTriggeringCommand:
[notif object];
if(!loginViewController.displayed) {
// if the view is not displayed then display it.
[[self topOfModalStack:self.window.rootViewController]
presentModalViewController:loginViewController
animated:YES];
}
loginViewController.displayed = YES;
}
}); // End of GC Dispatch block
}</span>
Nach dem Login kopieren
3) 视图控制器
在这个简单的应用中有个主要的视图控制器。RootViewController(参见下面的代码)继承了UITableViewController。当该控制器加载时,会创建并排队命令以加载用户的视频列表(又叫做YouTube 种子),并且会将控制流放回到主运行循环中以耐心等待命令的完成。
第一次调用总是失败的,因为这时用户还没有登录。CommandDispatchDemo/RootViewController.m 的requestVideoFeed 方法会启动加载视频列表的过程,如下所示:
<span style="font-family:Microsoft YaHei;font-size:14px;">(void)requestVideoFeed {
// create the command
GetFeed *op = [[GetFeedalloc] init];
// add the current authentication token to the command
CommandDispatchDemoAppDelegate *delegate =
(CommandDispatchDemoAppDelegate *)[[UIApplication
sharedApplication] delegate ];
op.token = delegate.token;
// register to hear the completion of the command</span>
Nach dem Login kopieren
<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>
Nach dem Login kopieren
<span style="font-family:Microsoft YaHei;font-size:14px;">// put it on the queue for execution
[op enqueueOperation];
[op release];
}</span>
Nach dem Login kopieren
注意,代码并不需要检查用户是否已经登录;在执行时命令会做检查。
gotFeed:方法会处理来自于YouTube 的最终返回数据。在此例中,requestVideoFeed:方法会将gotFeed:方法注册为完成通知的目标方法。如果调用成功,该方法会将数据加载
到表视图中,否则显示UIAlertView:
<span style="font-family:Microsoft YaHei;font-size:14px;">- (void) gotFeed:(NSNotification *)notif {
NSLog(@"User info = %@", notif.userInfo);
BaseCommand *op = notif.object;
if(op.status == kSuccess) {
self.feed = op.results;
// if entry is a single item, change it to an array,
// the XML reader cannot distinguish single entries
// from arrays with only one element
id entries = [[feed objectForKey:@"feed"] objectForKey:@"entry"];
if([entries isKindOfClass:[NSDictionary class]]) {
NSArray *entryArray = [NSArrayarrayWithObject:entries];
[[feed objectForKey:@"feed"] setObject:entryArrayforKey:@"entry"];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableViewreloadData];
});
} else {
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertView *alert = [[UIAlertViewalloc]
initWithTitle:@"No Videos"
message:@"The login to YouTube failed"
delegate:self
cancelButtonTitle:@"Retry"
otherButtonTitles:nil];
[alert show];
[alert release];
});
}
}
YouTubeVideoCell 是UITableViewCell 的子类,它会异步加载视频的缩略图。它通过LoadImageCommand 对象完成加载处理:
/**
* Start the process of loading the image via the command queue
**/
- (void) startImageLoad {
LoadImageCommand *cmd = [[LoadImageCommandalloc] init];
cmd.imageUrl = imageUrl;
// set the name to something unique
cmd.completionNotificationName = imageUrl;
[cmd listenForMyCompletion:self selector:@selector(didReceiveImage:)];
[cmdenqueueOperation];
[cmd release];
}</span>
Nach dem Login kopieren
这个类会改变完成通知名,这样它(也只有它)就可以接收到特定图片的通知了。否则,它还需要检查返回的通知来确定是否是之前发出的命令。
指挥调度模式的优雅之处在于能将应用中所有凌乱的异常处理逻辑和登录呈现逻辑与主视图控制器分离开来。当视图控制器发出命令时,会忽略掉所有的异常处理与认证处理,只是完成请求而已。只是发出请求,等待响应,然后处理响应。并不关心用户注册的请求是不是重试了5 次才成功。此外,服务请求代码并不需要知道请求来自于哪里,结果去向哪里;只是关注于执行调用并广播结果。
指挥调度模式还有其他优势,开发者一开始会编写一些代码并论证结果,如果顺利,那么会添加异常处理器,而这对之前的代码不会造成任何影响。此外,如果设计恰当,那么所有的网络服务调用都会使用相同的基础命令类,这会减少命令类的数量。
在通用应用中,可以通过异常监听器调整展示的视图,这样iPhone 上的错误显示界面就会适配于该平台,iPad 上的错误显示界面也会适配于更大的平台。
这种模式可以快速展示结果,对业务逻辑与异常处理进行关注分离,减少重复代码以及提供更好的用户体验。
5.4 小结
代码使用网络时会出现很多错误源,理解错误源有助于快速诊断并解析网络问题。借助于Reachability 框架,代码可以主动对变化的网络状况作出响应,从而避免不必要的网络错误的出现。在发出网络请求以及处理成功与失败的结果时遵循一致的模式,可以确保代码更加整洁、更加具有可维护性。
《iOS网络高级编程:iPhone和iPad的企业应用开发》试读电子书免费提供,有需要的留下邮箱,一有空即发送给大家。 别忘啦顶哦!
微信:qinghuashuyou
更多最新图书请点击查看哦
更多0
-
上一篇《C语言入门经典(第5版)》之C语言编程总结
-
下一篇11g
R1 & R2新特性介绍(针对DBA和开发者)