• iOS网络高级编程:iPhone和iPad的企业应用开发之错误处理


    
    

    本章内容

    ●    iOS应用中的网络错误源

    ●    检測网络的可达性

    ●    错误处理的经验法则

    ●    处理网络错误的设计模式

          到眼下为止,我们所介绍的iPhone与其它系统的网络交互都是基于一切正常这个如果。本章将会放弃这个如果,并深入探究网络的真实世界。

    在真实世界中,事情是会出错的。有时可能是很严重的错误:手机进入与离开网络、包丢掉或是延迟;网络基础设施出错;偶尔用户还会出错。

    如果一切正常,那么编写iOS应用就会简单不少,只是遗憾的是现实并不是如此。

    本章将会探讨导致网络操作失败的几个因素。介绍系统怎样将失败情况告知应用,应用又该怎样优雅地通知用户。

    此外,本章还将介绍怎样在不往应用逻辑中加入错误处理代码的情况下。以一种整洁且一致的方式处理错误的软件模式。

    5.1  理解错误源

          早期的iOS有个非常棒的天气预报应用。它在Wi-Fi和信号良好的蜂窝网络下使用正常,只是当网络质量不那么好时。这个天气预报应用就像感冒似的。在主屏幕上崩溃。有不少应用在出现网络错误时表现非常差劲。会疯狂弹出大量UIAlertView以告诉用户出现了“404 Error on Server X”等类似信息。还有非常多应用在网络变慢时界面会变得没有响应。

    这些情况的出现都是没有非常好地理解网络失败模式以及没有预期到可能的网络降级或是失败。

    假设想要避免这类错误并可以充分地处理网络错误,那么你首先须要理解它们的起源。

          考虑一个字节是怎样从设备发往远程server以及怎样从远程server将这个字节接收到设备,这个过程仅仅须要几百毫秒的时间,只是却要求网络设备都能正常工作才行。设备网络与网络互联的复杂性导致了分层网络的产生。分层网络将这样的复杂环境划分成了更加易于管理的模块。

    尽管这对程序猿非常有帮助。只是当数据在各个层之间流动时可能会产生之前提到的网络错误。图5-1展示了Internet协议栈的各个层次。


    图5-1

          每一层都会运行某种错误检測,这可能是数学意义上的、逻辑意义上的,或是其它类型的检測。比方。当网络接口层接收到某一帧时,它首先会通过错误校正码来验证内容,假设不匹配,那么错误就产生了。假设这个帧根本就没有到达。那就会产生超时或是连接重置。

    错误检測出如今栈的每一层,自下而上直到应用层,应用层则会从语法和语义上检查消息。

         在使用iOS中的URL载入系统时。尽管手机与server之间的连接可能会出现各种各样的问题,只是能够将这些原因分成3种错误类别,各自是操作系统错误、HTTP错误与应用错误。

    这些错误类别与创建HTTP请求的操作序列相关。图5-2展示了向应用server发出的HTTP请求(提供来自于企业网络的一些数据)的简单序列图。每块阴影区域都表示这3种错误类型的错误域。

    典型地。操作系统错误是由HTTPserver问题导致的。

    HTTP错误是由HTTPserver或应用server导致的。

    应用错误是由请求传输的数据或应用server查询的其它系统导致的。


    图5-2

          假设请求是安全的HTTPS请求。或是HTTPserver被重定向client,那么上面这个序列的步骤将会变得更加复杂。上述非常多步骤都包括着大量的子步骤。比方在建立TCP连接时涉及的SYN与SYN-ACK包序列等。以下将会具体介绍每一种错误类别。

    5.1.1  操作系统错误

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

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

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

    ●    没有应用监听目标port——在请求到达目标主机后。数据包会被发送到请求指定的port号。假设没有server监听这个port或是有太多的连接请求在排队。那么连接请求就会被拒绝。

    ●    无法解析目标主机名——假设无法解析目标主机名。那么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
    }
     
    }

          域属性依据产生错误代码的库或框架对这些错误代码进行隔离。借助域,框架开发人员无须操心覆盖错误代码。由于域属性定义了产生错误的框架。比方。框架A 与B 都会产生错误代码1,只是这两个错误代码会被每一个框架提供的唯一域值进行区分。因此,假设代码须要区分NSError 值。就必须对NSError 对象的code 与domain 属性进行比較。

    NSError 对象有例如以下3 个主要属性:
    ● code——标识错误的NSInteger 值。对于产生该错误的错误域来说,这个值是唯一的。
    ● domain —— 指定错误域的NSString 指针, 比方NSPOSIXErrorDomain 、NSOSStatusErrorDomain 及NSMachErrorDomain。
    ● userInfo——NSDictionary 指针,当中包括特定于错误的值。


    URL 载入系统中产生的非常多错误都来自于NSURLErrorDomain 域,代码值基本上都来自于CFNetworkErrors.h 中定义的错误代码。与iOS 提供的其它常量值一样,代码应该使用针对错误定义好的常量名而不是实际的错误代码值。比方,假设client无法连接到主机,那么错误代码是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>

          注意,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>
    这是传递给请求托付的终于消息,托付必须能识别出错误的原因并作出恰当的反应。在例如以下演示样例中。托付会向用户展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>

           上述代码以一种生硬且不友好的方式将错误展现给了用户。在iOS 人机界面指南(HiG)中。Apple 建议不要过度使用UIAlertViews。由于这会破坏设备的使用感受。

    5.3 节“优雅地处理网络错误”中介绍了怎样通过良好的用户界面以一种干净且一致的方式处理错误的模式。
          iOS 设备通信错误的还有一主要原因就是因为没有网络连接而导致设备无法訪问目标server。能够在尝试发起网络连接前检查一下网络状态,这样能够避免非常多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,单击+button加入框架。这时会出现框架选择界面。

    选择SystemConfiguration 框架,单击add button将其加入到项目中。
          例如以下代码片段会检查是否存在网络连接。

    不保证不论什么特定的主机或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>

         在某些情况下。你可能想要改动某些动作、禁用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>

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

    <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>

    上述代码将当前对象注冊为通知观察者,名为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>

          可达性还能够确定当前网络上某个特定的主机是否是可达的。

    能够通过该特性依据应用是处于内部隔离的网络上还是公开的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>

          请记住。该特性对目标主机的訪问有个来回。假设每一个请求都使用该特性,那就会极大添加应用的网络负载与延迟。Apple 建议不要在主线程上检測主机的可达性,由于尝试訪问主机可能会堵塞主线程。这会导致UI 被冻结。
          OS 错误首先就表明请求出现了问题。应用开发人员有时会忽略掉它们,只是这样做是有风险的。

    由于HTTP 使用了分层网络,这时HTTP 层或是应用层可能会出现其它类型的潜在失败情况。

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

    <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>

    响应的第一行有状态码。HTTP 响应能够带有消息体,当中包括友好、用户可读的信息。用于描写叙述发生的事情。你不应该将是否有响应体作为推断HTTP 请求成功与否的标志。


    一共同拥有5 类HTTP 错误:

    ● 信息性质的100 级别——来自于HTTP server的信息。表示请求的处理将会继续。只是带有警告。


    ● 成功的200 级别——server处理了请求。每一个200 级别的状态都表示成功请求的不同结果。

    比方。204 表示请求成功,只是没有向client返回负载。
    ● 重定向须要的300 级别——表示client必须运行某个动作才干继续请求,由于所需的资源已经移动了。URL 载入系统的同步请求方法会自己主动处理重定向而无须通知代码。假设应用须要对重定向进行自己定义处理,那么应该使用异步请求。
    ● client错误400 级别——表示client发出了server无法正确处理的错误数据。

    比方,未知的URL 或是不对的HTTP 头会导致这个范围内的错误。
    ● 下游错误500 级别——表示HTTP server与下游应用server之间出现了错误。比方,假设Web server调用了JavaEE 应用server,Servlet 出现了NullPointerException,那么client就会收到500 级别的错误。


          iOS 中的URL 载入系统会处理HTTP 头的解析。并能够轻松获取到HTTP 状态。

    假设代码通过HTTP 或HTTPS URL 发出了同步调用,那么返回的响应对象就是一个NSHTTPURLResponse 实例。NSHTTPURLResponse 对象的statusCode 属性会返回数值形式的请求的HTTP 状态。

    例如以下代码演示了对NSError 对象以及从HTTP server返回的成功状态的验证:

    <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>

          假设请求的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>
    要想了解关于HTTP 状态码的权威信息。请參考W3 RFC 2616。网址是http://www.w3.org/Protocols/rfc2616/rfc2616.html。
    5.1.3 应用错误
          本节将会介绍网络协议栈的下一层(应用层)产生的错误。应用错误不同于OS 错误或HTTP 错误。由于并没有针对这些错误的标准值或是原因的集合。这些错误是由执行在服
    务层之上的业务逻辑和应用造成的。在某些情况下。错误可能是代码问题,比方异常。只是在其它一些情况下,错误可能是语义错误,比方向服务提供了无效的账号等。对于前者来说,建议生成HTTP 500 级别的错误;对于后者来说,应该在应用负载中返回错误码。
          比方,假设用户尝试从账户中转账的金额超出了账户的可用剩余金额,那么手机银行就应该报告应用错误。假设发出了这种请求,那么OS 会说请求成功发送并接收到了响应。HTTP server会报告接收到了请求并发出了响应。只是应用层必须报告这笔交易失败。

    报告应用错误的最佳实践是将应用的负载数据封装在标准信封中。信封中含有一致的应用错误位置信息。

    在上述资金转账演示样例中,成功的转账响应的业务负载应该例如以下所看到的:

    <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>
    <span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">
    </span></span>


    响应包括了源账号与目标账号、转账的资金数额及确认号。

    直接将错误码与错误消息放到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>


    报告错误的UI 代码是非常easy重用的,由于错误信息总是位于响应负载的error 属性中。此外。实际的交易负载处理得到了简化。由于它总是位于同样的属性名之下。
    不管请求失败的原因是什么,OS、HTTP 层还是应用。应用都必须能知道怎样作出响应。你应该在开发时就提前考虑好应用全部的失败模式,并设计好一致的方式来检測并响应错误。



    5.2 错误处理的经验法则
    错误可能是由多种原因造成的,最佳处理方式也随编写的应用不同而不同。尽管非常复杂,只是有一些经验法则能够帮助处理错误原因不可控的本质。
    5.2.1 在接口契约中处理错误
    在设计服务接口时,仅仅指定输入、输出与服务操作的做法是不对的。

    接口契约还应该指定怎样向client发送错误信息。服务接口应该使用业界标准方式在可能的情况下传递错误信息。

    比方。server不应该为服务端失败定义新的HTTP 状态值;相反,应该使用恰当的500 级别的状态。假设使用了标准值,那么client与服务端开发人员就能对怎样传递错误信息达成共识。

    应用绝不应该依赖于非标准的状态或是其它属性值来确定错误出现与否。
    应用开发人员也不应该依赖于当前server软件栈的行为来决定该怎样处理错误。在部署了iOS 应用后,server软件栈可能会因为未来的升级或替换而改变行为。


    5.2.2 错误状态可能不对
    移动网络有例如以下有别于传统Web 应用错误的不那么明显的行为:模糊不清的错误报告。

    从移动设备发往server的不论什么网络请求都有3 种可能的结果:
    ● 设备全然可以确认操作是成功的。比方。NSError 与HTTP 状态值都表明成功。返回的负载包括语义上正确的信息。
    ● 设备全然可以确认操作是失败的。

    比方。返回的应用负载包括来自于server的特定于本次操作的失败标识。


    ● 设备模糊地确认操作是失败的。比方,移动应用发出HTTP 请求以在两个账户间转账。请求被银行系统接收并正确地处理;然而,因为网络失败应答却丢失了,NSURLConnection 报告超时。超时发生了,但却是在转账请求成功之后发生的。假设重试该操作。那就会导致反复转账。可能还会造成账户透支。

    第3 种场景会导致应用出现意外和检測不到的错误行为。假设应用开发人员不知道第3种场景的存在。那么他们可能就会错误地假设操作失败,然后不小心重试已经成功的操作。

    知道整个操作失败还不够,开发人员必须考虑导致请求失败的原因,以及自己主动重试每一个失败的请求是否是恰当的。


    5.2.3 验证负载
    应用开发人员不应该觉得假设没有OS 错误或HTTP 错误,负载就是有效的。在非常多场景下,请求似乎是成功的,只是负载却是无效的。client与server之间传递的负载都一种验证机制。JSON 与XML 就是具备了验证机制的负载格式。只是以逗号分隔的值文件与HTML 就没有这样的机制。

    5.2.4 分离错误与正常的业务状况
    服务契约不应该将正常的业务状况报告为错误。比方有个用户,因为可能的欺诈导致账户被锁定。锁定状态应该在数据负载中进行报告而不应该当作错误情况。

    分离错误与正常的业务状况会让代码保持恰当的关注分离。仅仅有当出现故障时才应该将之看成错误。


    5.2.5 总是检查HTTP 状态
    总是检查HTTP 响应中的HTTP 状态,理解成功的状态值。甚至向同样的服务发出反复的调用也是如此。server的状态可能随时会发生变化,甚至在并行的调用间也是如此。
    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>

    5.3 优雅地处理网络错误
    iOS 简化了网络通信。只是对可能发生的全部类型的错误与边界条件作出响应则不是那么轻松的事情。常见的做法是在网络代码中放置钩子来高速查看结果。接下来再对全部的错误情况进行处理。

    对于非移动应用来说,通常能够使用这种方式。由于来自工作站的网络连接是可预測的。假设在应用载入时有网络。那么当用户载入下一个页面时基本上也会有网络。

    绝大多数情况都是这种,开发人员能够依赖浏览器向用户显示消息。

    假设在移动应用中没有及时加入异常处理。那么当后面遇到新的错误源时就须要大幅重构网络代码。
    本节将会介绍一种设计模式。用来创建一个优雅且健壮的异常处理框架,而且在未来遇到新的错误时差点儿不须要做什么工作就能非常好地进行扩展。考虑例如以下3 个移动通信中的主要异常情况:
    ● 因为设备没有充分的网络连接导致远程server不可达。
    ● 因为OS 错误、HTTP 错误或是应用错误导致远程server返回错误响应。


    ● server须要认证,而设备尝试发出未认证的请求。
    随着可能的异常数量呈现出线性增长。处理这些异常的代码量则呈指数级增长。假设代码要在每一类请求中处理全部这些错误,那么代码的复杂性与数量就会呈指数级增长。本节将要介绍的模式会将这样的指数级的曲线压成线性曲线。


    5.3.1 设计模式介绍
    本节介绍的模式联合使用了指挥调度模式与广播通知。该模式包括例如以下类型的对象:
    ● 控制器
    ● 命令对象
    ● 异常监听器
    ● 命令队列
    以下从高层次来介绍每一类对象的行为。
    1. 对象说明
    以下介绍构成指挥调度模式的对象的特性及属性。
    控制器
    控制器通常指的是视图控制器,用来请求数据并处理结果。

    在该设计模式中。控制器无须包括不论什么异常处理逻辑。唯一须要控制器处理的错误情况就是成功完毕或是全然不可恢复的服务失败。

    在不可恢复失败这个场景中。控制器一般会将自己从视图栈中弹出,由于用户这时已经收到接下来要介绍的异常监听器对象发出的失败通知了。控制器会创建命令并监听命令的完毕情况。


    命令对象
    命令对象与应用运行的不同网络交易相关。

    检索图片、从指定的REST 端点处获取JSON 数据或是向服务发出POST 信息等都是命令对象请求。

    命令对象是NSOperation 的子类。

    因为命令对象中的大多数逻辑都与其它类型的命令对象同样,因此能够创建父类命令象来处理,让特定的命令继承该逻辑。命令对象具有例如以下属性:

    ● 完毕通知名——在iOS 中。控制器会将自身注冊为该通知名的观察者。当服务调用成功返回时,命令对象会通过NSNotificationCenter 使用该名字来广播通知。尽管该名字对于命令类来说一般是唯一的,只是在某些情况下,假设有多个控制器发出同样类型的命令(区分不同的响应),那么这个名字针对于特定的实例将是唯一的。


    ● server错误异常通知名——特定的异常处理器对象会监听该通知。当server超时、返回与认证相关的OS 错误或HTTP 错误时。命令对象会通过NSNotificationCenter并使用该名字来广播消息。通常情况下。全部的命令类会共享同样的异常名,因此也会共享同样的异常监听器。只是不同的命令类可能须要使用不同的异常监听器,而且有不同的server错误异常名。
    ● 可达性异常通知名——当检測到无法到达Internet 或目标主机时,命令对象会生成该类型的通知。还有一个异常监听器能够监听该类型的异常。在某些应用中,这类常是不须要的。由于server错误异常监听器会处理可达性异常。
    ● 认证异常通知名——假设确定用户没有认证或是server报告了未认证状态,那么命令对象可能会产生该类型的通知。

    第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 错误。那就广播一条server异常通知。假设是认证错误,那就广播一条认证异常通知。
    (6) 解析结果,參见第4 章。


    (7) 广播一条成功状态的完毕通知。
    当命令对象广播通知时,不管是成功还是其它通知。都须要创建字典对象,字典对象中包括自身的副本、调用状态与返回的数据。以此作为调用的结果。自身复制是有必要的,由于NSOperation 实例仅仅能运行一次。稍后将会介绍,在监听器处理异常时,命令可能会被再次提交。


    同步请求API 很适合于该模式,由于命令是在后台线程而非主线程中运行的。

    假设请求发出或是返回的数据量超出期望在内存中处理的数据量。应用就须要使用异步请求了。

    由于NSOperation 的主要功能是一个方法。操作必须实现并发锁来堵塞main 方法,直到异步调用完毕为止。


    异常监听器行为
    这样的模式之所以如此强大,异常监听器功不可没。

    这些对象通常都是由应用托付创建的,驻留在内存中并监听着通知。当接收到通知时,监听器会通知用户。还可能会接收来自用户的响应。当异常发生时,通知中包括了触发该异常的命令副本,当用户作出响应后。监听器一般会再次将命令发回到队列中并重试。

    关于异常监听器有趣的一点是:因为多个命令可能同一时候发生,因此在用户响应第一个异常时可能还会同一时候产生多个异常通知。出于这一点。异常监听器必须收集异常通知,然后在用户响应完第一个异常后又一次提交全部的触发命令。这个错误集合能够避免一种常见的应用行为不当——用户被同样问题触发的多个UIAlertView 连续轰炸。

    server异常的流程例如以下所看到的:
    (1) 呈现一个美丽的模态对话框,列出错误信息并让用户选择取消或是重试。
    (2) 收集可能被广播的其它server异常。


    (3) 假设用户选择重试,那么关闭对话框并又一次提交全部收集到的命令。


    (4) 假设用户选择取消,那么关闭对话框。监听器应该将全部收集到的命令的完毕状态设为失败,然后让每一个命令广播一条完毕通知。
    可达性异常的流程例如以下所看到的:
    (1) 呈现一个美丽的模态对话框,通知用户须要网络连接。
    (2) 收集可能会被广播的其它服务异常。
    (3) 监听可达性变更。当网络可达时。关闭对话框并又一次提交收集到的命令。

    认证异常的流程略微有点复杂。

    请记住,命令之间是独立的,在随意时刻可能会有多个命令同一时候发生。

    认证流程并不会生成认证异常通知,流程例如以下所看到的:
    (1) 呈现一个模态登录视图。
    (2) 继续收集因为认证错误导致的失败命令。
    (3) 假设用户取消,那么监听器应该针对收集到的命令使用失败状态发送一条完毕通知。
    (4) 假设用户提供了认证信息,那么创建一个登录命令,将其放到命令队列中。
    (5) 等待登录命令的完毕通知。
    (6) 假设因为username/password不匹配而导致登录失败,那么回到步骤(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>

    在上述代码清单中。通过self 调用的非常多方法都是在BaseCommand 父类中实现的。GetFeed 命令就是指挥调度模式的原型。

    main 方法会对用户登录进行检查,由于假设这个调用失败了,那就不是必需再调用server了。假设用户已经登录。那么代码就会构建请求。将认证头加入到请求中。然后发送一条同步请求。

    代码的最后一部分会调用一个父类方法来确定调用是否成功。

    该方法使用来自于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:向用户请求username与password。

    位于视图栈的顶部。直到用户成功登录为止。
    ● 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>
    应用托付会将自身注冊为网络错误与须要登录通知的监听器(如代码清单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>

    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>
    <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>
    <span style="font-family:Microsoft YaHei;font-size:14px;">// put it on the queue for execution
    [op enqueueOperation];
    [op release];
    }</span>

    注意,代码并不须要检查用户是否已经登录;在运行时命令会做检查。
    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>
    这个类会改变完毕通知名,这样它(也仅仅有它)就能够接收到特定图片的通知了。否则,它还须要检查返回的通知来确定是否是之前发出的命令。


    指挥调度模式的优雅之处在于能将应用中全部凌乱的异常处理逻辑和登录呈现逻辑与主视图控制器分离开来。当视图控制器发出命令时,会忽略掉全部的异常处理与认证处理。仅仅是完毕请求而已。

    仅仅是发出请求。等待响应,然后处理响应。

    并不关心用户注冊的请求是不是重试了5 次才成功。此外,服务请求代码并不须要知道请求来自于哪里,结果去向哪里;仅仅是关注于运行调用并广播结果。

    指挥调度模式还有其它优势。开发人员一開始会编写一些代码并论证结果,假设顺利,那么会加入异常处理器。而这对之前的代码不会造成不论什么影响。

    此外,假设设计恰当。那么全部的网络服务调用都会使用同样的基础命令类,这会降低命令类的数量。

    在通用应用中。能够通过异常监听器调整展示的视图,这样iPhone 上的错误显示界面就会适配于该平台,iPad 上的错误显示界面也会适配于更大的平台。
    这样的模式能够高速展示结果,对业务逻辑与异常处理进行关注分离,降低反复代码以及提供更好的用户体验。



    5.4 小结
    代码使用网络时会出现非常多错误源,理解错误源有助于高速诊断并解析网络问题。

    借助于Reachability 框架,代码能够主动对变化的网络状况作出响应,从而避免不必要的网络错误的出现。

    在发出网络请求以及处理成功与失败的结果时遵循一致的模式,能够确保代码更加整洁、更加具有可维护性。



    《iOS网络高级编程:iPhone和iPad的企业应用开发》试读电子书免费提供,有须要的留下邮箱,一有空即发送给大家。 别忘啦顶哦!


    微信:qinghuashuyou  
    很多其它最新图书请点击查看哦



    很多其它0


  • 相关阅读:
    BPM实例方案分享:表单子表自动填入数据
    H3 BPM循环子表相关方法介绍
    H3 BPM前后台交互方法介绍
    Web Service Adapter简介:
    H3 BPM钉钉接入配置
    H3 BPM 跨平台表单发起详解
    H3 BPM门户操作说明及实例介绍
    H3 BPM报销流程开发示例
    Photon Cloud Networking: OnPhotonSerializeView Not Firing
    unity 事件顺序及功能说明
  • 原文地址:https://www.cnblogs.com/gcczhongduan/p/5229605.html
Copyright © 2020-2023  润新知