Advanced iOS Network Programming: Error Handling for iPhone and iPad Enterprise Application Development_PHP Tutorial

WBOY
Release: 2016-07-13 10:22:36
Original
1687 people have browsed it

Advanced iOS Network Programming: Error Handling for iPhone and iPad Enterprise Application Development

Copy after login

Contents of this chapter

● Network error sources in iOS apps

● Check network reachability

● Rules of thumb for error handling

● Design patterns for handling network errors

So far, the network interactions between iPhone and other systems we have introduced are based on the assumption that everything is normal. This chapter will abandon this assumption and delve into the real world of networking. In the real world, things go wrong, sometimes catastrophically: phones enter and leave the network, packets are dropped or delayed; network infrastructure goes wrong; occasionally users make mistakes. If everything went fine, writing iOS apps would be a lot easier, but unfortunately that's not the case. This chapter will explore several factors that lead to network operation failure, introduce how the system notifies the application of the failure, and how the application can gracefully notify the user. Additionally, this chapter introduces software patterns for handling errors in a clean and consistent manner without adding error-handling code to your application logic.

5.1 Understanding error sources

There was a great weather forecast app in the early days of iOS. It works fine on Wi-Fi and a good cellular network, but when the network quality is not that good, the weather forecast app crashes on the home screen like a cold. There are many applications that perform poorly when network errors occur, and will pop up a large number of UIAlertViews to tell users that "404 Error on Server X" and other similar messages have occurred. There are also many apps whose interfaces become unresponsive when the network slows down. These situations occur because network failure modes are not well understood and possible network degradation or failure is not anticipated. If you want to avoid these types of errors and be able to handle network errors adequately, you first need to understand their origins.

Consider how a byte is sent from the device to the remote server and how the byte is received from the remote server to the device. This process only takes a few hundred milliseconds, but it requires the network equipment to work properly. The complexity of device networks and network interconnections has led to the creation of hierarchical networks. Hierarchical networking divides this complex environment into more manageable modules. While this is helpful for programmers, the network errors mentioned earlier can occur when data flows between layers. Figure 5-1 shows the various layers of the Internet protocol stack.


Figure 5-1

Each layer performs some kind of error detection, which may be mathematical, logical, or other types of detection. For example, when the network interface layer receives a certain frame, it will first verify the content through the error correction code. If it does not match, an error occurs. If the frame never arrives, a timeout or connection reset will occur. Error detection occurs at every layer of the stack, from the bottom up to the application layer, which checks messages syntactically and semantically.

When using the URL loading system in iOS, although various problems may occur in the connection between the mobile phone and the server, these reasons can be divided into 3 error categories, namely operating system errors and HTTP errors. with application errors. These error categories relate to the sequence of operations that create an HTTP request. Figure 5-2 shows a simple sequence diagram of an HTTP request to an application server providing some data from the corporate network. Each shaded area represents the error domain of these three error types. Typically, operating system errors are caused by HTTP server problems. HTTP errors are caused by the HTTP server or application server. Application errors are caused by requests for data to be transferred or by other systems queried by the application server.


Figure 5-2

If the request is a secure HTTPS request, or the HTTP server is redirecting the client, then the above sequence of steps will become more complicated. Many of the above steps contain a large number of sub-steps, such as the SYN and SYN-ACK packet sequences involved in establishing a TCP connection. Each error category is described in detail below.

5.1.1 Operating System Error

Operating system errors are caused by packets not reaching their intended destination. Packets may be part of establishing a connection, or they may be in the middle of establishing a connection. OS errors may be caused by:

● No network - If the device does not have a data network connection, the connection attempt will quickly be rejected or fail. These types of errors can be detected through the Reachability framework provided by Apple, which is described later in this section.

● Unable to route to target host - The device may have a network connection, but the connected target may be on an isolated network or offline. These errors are sometimes detected quickly by the operating system, but may also cause the connection to time out.

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

● 无法解析目标主机名——如果无法解析目标主机名,那么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
}
 
}
Copy after login

域属性根据产生错误代码的库或框架对这些错误代码进行隔离。借助域,框架开发者无须担心覆盖错误代码,因为域属性定义了产生错误的框架。比如,框架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>
Copy after login

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

上述代码以一种生硬且不友好的方式将错误展现给了用户。在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

Advanced iOS Network Programming: Error Handling for iPhone and iPad Enterprise Application Development_PHP Tutorial

选定好项目目标后,找到设置中的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>
Copy after login

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

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

<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>
Copy after login

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

可达性还可以确定当前网络上某个特定的主机是否是可达的。可以通过该特性根据应用是处于内部隔离的网络上还是公开的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>
Copy after login

请记住,该特性对目标主机的访问有个来回。如果每个请求都使用该特性,那就会极大增加应用的网络负载与延迟。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>
Copy after login

响应的第一行有状态码。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>
Copy after login

如果请求的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>
Copy after login
要想了解关于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>
Copy after login
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">
</span></span>
Copy after login


响应包含了源账号与目标账号、转账的资金数额及确认号。直接将错误码与错误消息放到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>
Copy after login


UI code that reports errors is easily reusable because the error information is always located in the error attribute of the response payload. Additionally, the actual transaction load handling is simplified as it is always under the same attribute name.
Regardless of the reason for the request failure, be it the OS, the HTTP layer, or the application, the application must know how to respond. You should think through all of your application's failure modes ahead of time during development, and design a consistent way to detect and respond to errors.

5.2 Rules of thumb for error handling
Errors can occur for a variety of reasons, and the best way to handle them will vary depending on the application you're writing. Although complex, there are some rules of thumb that can help deal with the uncontrollable nature of error causes.
5.2.1 Handling errors in interface contracts
When designing a service interface, it is incorrect to only specify inputs, outputs, and service operations. The interface contract should also specify how error messages are sent to the client. Service interfaces should use industry standard methods to communicate error information where possible. For example, servers should not define new HTTP status values ​​for server-side failures; instead, they should use the appropriate 500-level status. If standard values ​​are used, client and server developers can agree on how to communicate error messages. Applications should never rely on nonstandard status or other property values ​​to determine whether an error occurred.
Application developers should also not rely on the behavior of the current server software stack to determine how to handle errors. After an iOS app is deployed, the server software stack may change behavior due to future upgrades or replacements.
5.2.2 Error status may be incorrect
Mobile web has the following less obvious behavior that differs from traditional web application errors: vague error reporting. Any network request from a mobile device to the server has 3 possible outcomes:
● The device is fully capable of confirming that the operation was successful. For example, NSError and HTTP status values ​​both indicate success, and the returned payload contains semantically correct information.
● The device is fully capable of confirming that the operation failed. For example, the returned application payload contains a failure identifier from the server that is specific to this operation.
● The device vaguely acknowledges that the operation failed. For example, a mobile app makes an HTTP request to transfer money between two accounts. The request was received by the banking system and processed correctly; however, the response was lost due to network failure and NSURLConnection reported a timeout. The timeout occurred, but after the transfer request was successful. If you retry the operation, it will result in a duplicate transfer and possibly an overdraft on your account. Scenario 3 can lead to unexpected and undetected erroneous behavior of the application. If the application developer is unaware of the existence of scenario 3, then they may mistakenly assume that the operation failed and then accidentally retry the operation that has already succeeded. It is not enough to know that the entire operation failed, the developer must consider what caused the request to fail and whether it is appropriate to automatically retry each failed request.
5.2.3 Verification payload
Application developers should not assume that the payload is valid if there are no OS errors or HTTP errors. In many scenarios, the request appears to be successful, but the payload is invalid. The payload passed between client and server is a verification mechanism. JSON and XML are payload formats that have validation mechanisms, but comma-separated value files and HTML do not.
5.2.4 Separate errors from normal business conditions
Service contracts should not report normal business conditions as errors. For example, if a user's account is locked due to possible fraud, the lock status should be reported in the data payload and should not be treated as an error condition. Separating errors from normal business conditions will allow your code to maintain proper separation of concerns. Only when something goes wrong should it be considered an error.
5.2.5 Always check HTTP status
Always check the HTTP status in the HTTP response to understand the success status value, even when making repeated calls to the same service. The server's state may change at any time, even between parallel calls.
5.2.6 Always check NSError value
Application code should always check the returned NSError value to ensure that no problems occurred at the OS level. You should do this even if you know your app will always run on a Wi-Fi network with good signal. Everything has the potential to go wrong, and code needs to be defensive when dealing with the network.
5.2.7 Use a consistent approach to handling errors
There are many reasons for network errors, it is difficult to list them all, and the diversity and scope of the impact are also very large. When designing your application, don't just focus on a consistent user interface pattern or a consistent naming pattern. You should also design consistent patterns for handling network errors. The pattern should take into account all types of errors that the application may encounter. If these errors are not handled in a consistent pattern internally by the application, the application cannot report these errors to the user in a consistent manner.
5.2.8 Always set timeout
In iOS, the default timeout interval for HTTP requests is 4 minutes, which is too long for mobile apps and most users will not wait 4 minutes in any app.开发者需要选择合理的超时时间,方式是评估网络请求的可能响应时间,然后将最差的网络场景下的网络延迟考虑进去。如下示例展示了如何创建具有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>
Copy after login

5.3 优雅地处理网络错误
iOS 简化了网络通信,不过对可能发生的所有类型的错误与边界条件作出响应则不是那么轻松的事情。常见的做法是在网络代码中放置钩子来快速查看结果,接下来再对所有的错误情况进行处理。对于非移动应用来说,通常可以使用这种方式,因为来自工作站的网络连接是可预测的。如果在应用加载时有网络,那么当用户加载下一个页面时基本上也会有网络。绝大多数情况都是这样的,开发者可以依赖浏览器向用户显示消息。如果在移动应用中没有及时添加异常处理,那么当后面遇到新的错误源时就需要大幅重构网络代码。
本节将会介绍一种设计模式,用来创建一个优雅且健壮的异常处理框架,并且在未来遇到新的错误时几乎不需要做什么工作就能很好地进行扩展。考虑如下3 个移动通信中的主要异常情况:
● 由于设备没有充分的网络连接导致远程服务器不可达。
● 由于OS 错误、HTTP 错误或是应用错误导致远程服务器返回错误响应。
● 服务器需要认证,而设备尝试发出未认证的请求。
随着可能的异常数量呈现出线性增长,处理这些异常的代码量则呈指数级增长。如果代码要在每一类请求中处理所有这些错误,那么代码的复杂性与数量就会呈指数级增长。本节将要介绍的模式会将这种指数级的曲线压成线性曲线。
5.3.1 设计模式介绍
本节介绍的模式联合使用了指挥调度模式与广播通知。该模式包含如下类型的对象:
● 控制器
● 命令对象
● 异常监听器
● 命令队列
下面从高层次来介绍每一类对象的行为。
1. 对象说明
下面介绍构成指挥调度模式的对象的特性及属性。
控制器
控制器通常指的是视图控制器,用来请求数据并处理结果。在该设计模式中,控制器无须包含任何异常处理逻辑。唯一需要控制器处理的错误情况就是成功完成或是完全不可恢复的服务失败。在不可恢复失败这个场景中,控制器通常会将自己从视图栈中弹出,因为用户这时已经收到接下来要介绍的异常监听器对象发出的失败通知了。控制器会创建命令并监听命令的完成情况。
命令对象
命令对象与应用执行的不同网络交易相关。检索图片、从指定的REST 端点处获取JSON 数据或是向服务发出POST 信息等都是命令对象请求。命令对象是NSOperation 的子类。由于命令对象中的大多数逻辑都与其他类型的命令对象相同,因此可以创建父类命令象来处理,让特定的命令继承该逻辑。命令对象具有如下属性:
● 完成通知名——在iOS 中,控制器会将自身注册为该通知名的观察者。当服务调用成功返回时,命令对象会通过NSNotificationCenter 使用该名字来广播通知。虽然该名字对于命令类来说通常是唯一的,不过在某些情况下,如果有多个控制器发出相同类型的命令(区分不同的响应),那么这个名字针对于特定的实例将是唯一的。
● 服务器错误异常通知名——特定的异常处理器对象会监听该通知。当服务器超时、返回与认证相关的OS 错误或HTTP 错误时,命令对象会通过NSNotificationCenter并使用该名字来广播消息。通常情况下,所有的命令类会共享相同的异常名,因此也会共享相同的异常监听器。不过不同的命令类可能需要使用不同的异常监听器,并且有不同的服务器错误异常名。
● 可达性异常通知名——当检测到无法到达Internet 或目标主机时,命令对象会生成该类型的通知。另一个异常监听器可以监听该类型的异常。在某些应用中,这类常是不需要的,因为服务器错误异常监听器会处理可达性异常。
● 认证异常通知名——如果确定用户没有认证或是服务器报告了未认证状态,那么命令对象可能会产生该类型的通知。第3 个异常监听器会等待该类型通知的出现。认证通知名通常会在应用的所有通知中共享。
● 自定义属性——这些属性特定于发出的请求。控制器通常会提供这些值,因为它们特定于服务调用所需的业务数据,而且不同的调用数据也是不同的。异常监听器
一般来说,每个异常监听器都是由应用委托实例化的,位于后台并等待着特定类型的通知。In many cases, exception listeners present a modal view controller when a notification is received, which is described in the "Exception Listener Behavior" section.
Command Queue
The controller will submit the command to the command queue for processing, and the application may have one or more command queues. In iOS, the command queue is a subclass of NSOperationQueue. The main queue should not be used as a command queue because its operations run on the user interface thread, which affects the user experience when performing long-running operations.
NSOperationQueues provide built-in functionality for managing active operations and dependencies between operations.
2. Objects Each of the above objects plays a different role in the successful completion of online transactions. Their respective roles in this mode are introduced below.
Controller Behavior
Controllers focus on executing UI and business logic. When the controller wants to get data from the service, it should take the following actions:
(1) Create a network command object.
(2) Initialization request for specific attributes of the command object.
(3) Register as an observer for command completion.
(4) Push the command to the operation queue to prepare for execution.
(5) Wait for NSNotificationCenter to send completion notification.
When the operation is completed, the controller will receive the completion notification and take the following actions:
(1) Check the operation status to see if the operation is successful.
(2) If successful, the controller processes the received data. The data received is provided to the controller through the userInfo property of the NSNotification object. NSOperationQueues execute NSOperation objects on their own threads. When the operation is completed, an NSNotification is sent through NSNotificationCenter. The notification callback method will be called on the thread that the NSOperation is running on, which in this case ensures that it does not enter the main thread. If the controller manipulates the UI, then these changes need to be made on the main thread, usually through Grand CentralDispatch (GCD).
(3) If unsuccessful, the controller has many options depending on the application requirements. For example, you can pop yourself out of the view stack or update the UI to indicate that data is unavailable. Controllers should not retry or display modal warnings because
These actions are the responsibility of the exception listener. (4) The controller should remove itself from observers of command completion notifications. In some cases, this is not necessary if the controller wants to monitor other data from the same type of command. Note that the controller does not contain any logic to handle retries, timeouts, authentication, or reachability; this logic is implemented by command and exception listeners.
If the controller wants to ensure that only it receives the returned data, it should change the notification name before placing it on the queue to a value unique to that command object instance, and then listen for notifications with that name. Command Object Behavior The command object is responsible for calling the target service and broadcasting the results of the service call. Generally speaking, the command object needs to perform the following steps:
(1) Check reachability. If the network is unreachable, a reachability exception notification is broadcast.
(2) If necessary, check the certification status. If the user is not authorized, a reachability exception notification is broadcast.
(3) Build network requests using custom attributes provided by the controller. Typically, the endpoint URL is a static property of the command object class or is loaded from the configuration subsystem.
(4) Use synchronous request method to issue network requests. See Chapter 3, Section 3.3.2 “Synchronization Requests” for details.
(5) Check the request status. If the status is an OS error or HTTP error, a server exception notification is broadcast. If it is an authentication error, an authentication exception notification is broadcast.
(6) For analysis results, see Chapter 4.
(7) Broadcast a completion notification of successful status.
When a command object broadcasts a notification, whether it is a success or other notification, a dictionary object needs to be created. The dictionary object contains a copy of itself, the calling status and the returned data as the result of the call. Replicating itself is necessary because an NSOperation instance can only be executed once. As will be described later, the command may be resubmitted while the listener handles the exception.
The synchronous request API is well suited for this pattern because commands are executed in a background thread rather than the main thread. If a request makes or returns more data than can be expected to be processed in memory, the application needs to use asynchronous requests. Because the main function of NSOperation is a method, the operation must implement a concurrent lock to block main method until the asynchronous call completes.
Exception listener behavior
The exception listener plays an important role in making this model so powerful. These objects are typically created by application delegates, reside in memory, and listen for notifications. When a notification is received, the listener notifies the user and may also receive a response from the user. When an exception occurs, the notification contains a copy of the command that triggered the exception. When the user responds, the listener typically sends the command back to the queue and tries again. An interesting point about exception listeners is that since multiple commands may occur at the same time, multiple exception notifications may be generated at the same time when the user responds to the first exception.出于这一点,异常监听器必须收集异常通知,然后在用户响应完第一个异常后重新提交所有的触发命令。这个错误集合可以避免一种常见的应用行为不当——用户被相同问题触发的多个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>
Copy after login

在上述代码清单中,通过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>
Copy after login
应用委托会将自身注册为网络错误与需要登录通知的监听器(如代码清单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>
Copy after login

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

注意,代码并不需要检查用户是否已经登录;在执行时命令会做检查。
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>
Copy after login
这个类会改变完成通知名,这样它(也只有它)就可以接收到特定图片的通知了。否则,它还需要检查返回的通知来确定是否是之前发出的命令。
指挥调度模式的优雅之处在于能将应用中所有凌乱的异常处理逻辑和登录呈现逻辑与主视图控制器分离开来。当视图控制器发出命令时,会忽略掉所有的异常处理与认证处理,只是完成请求而已。只是发出请求,等待响应,然后处理响应。并不关心用户注册的请求是不是重试了5 次才成功。此外,服务请求代码并不需要知道请求来自于哪里,结果去向哪里;只是关注于执行调用并广播结果。 指挥调度模式还有其他优势,开发者一开始会编写一些代码并论证结果,如果顺利,那么会添加异常处理器,而这对之前的代码不会造成任何影响。此外,如果设计恰当,那么所有的网络服务调用都会使用相同的基础命令类,这会减少命令类的数量。 在通用应用中,可以通过异常监听器调整展示的视图,这样iPhone 上的错误显示界面就会适配于该平台,iPad 上的错误显示界面也会适配于更大的平台。 这种模式可以快速展示结果,对业务逻辑与异常处理进行关注分离,减少重复代码以及提供更好的用户体验。

5.4 Summary
There are many sources of errors when code uses the network. Understanding the sources of errors can help quickly diagnose and resolve network problems. With the Reachability framework, code can proactively respond to changing network conditions to avoid unnecessary network errors. Following a consistent pattern when making network requests and handling success and failure results ensures that your code is cleaner and more maintainable.

The trial e-book "Advanced iOS Network Programming: Enterprise Application Development for iPhone and iPad" is provided for free. If you need it, please leave your email address and we will send it to you as soon as you are free. Don’t forget to like it!
WeChat: qinghuashuyou Please click to view more latest books


More 0
source:php.cn
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template
About us Disclaimer Sitemap
php.cn:Public welfare online PHP training,Help PHP learners grow quickly!