iPhone和iPad的企業(yè)應用開發(fā)之錯誤處理
5.1 理解錯誤源
早期的iOS有個很棒的天氣預報應用。它在Wi-Fi和信號良好的蜂窩網(wǎng)絡下使用正常,不過當網(wǎng)絡質量不那么好時,這個天氣預報應用就像感冒似的,在主屏幕上崩潰。有不少應用在出現(xiàn)網(wǎng)絡錯誤時表現(xiàn)很差勁,會瘋狂彈出大量UIAlertView以告訴用戶出現(xiàn)了“404 Error on Server X”等類似信息。還有很多應用在網(wǎng)絡變慢時界面會變得沒有響應。這些情況的出現(xiàn)都是沒有很好地理解網(wǎng)絡失敗模式以及沒有預期到可能的網(wǎng)絡降級或是失敗。如果想要避免這類錯誤并能夠充分地處理網(wǎng)絡錯誤,那么你首先需要理解它們的起源。
考慮一個字節(jié)是如何從設備發(fā)往遠程服務器以及如何從遠程服務器將這個字節(jié)接收到設備,這個過程只需要幾百毫秒的時間,不過卻要求網(wǎng)絡設備都能正常工作才行。設備網(wǎng)絡與網(wǎng)絡互聯(lián)的復雜性導致了分層網(wǎng)絡的產(chǎn)生。分層網(wǎng)絡將這種復雜環(huán)境劃分成了更加易于管理的模塊。雖然這對程序員很有幫助,不過當數(shù)據(jù)在各個層之間流動時可能會產(chǎn)生之前提到的網(wǎng)絡錯誤。圖5-1展示了Internet協(xié)議棧的各個層次。
每一層都會執(zhí)行某種錯誤檢測,這可能是數(shù)學意義上的、邏輯意義上的,或是其他類型的檢測。比如,當網(wǎng)絡接口層接收到某一幀時,它首先會通過錯誤校正碼來驗 證內容,如果不匹配,那么錯誤就產(chǎn)生了。如果這個幀根本就沒有到達,那就會產(chǎn)生超時或是連接重置。錯誤檢測出現(xiàn)在棧的每一層,自下而上直到應用層,應用層 則會從語法和語義上檢查消息。
在使用iOS中的URL加載系統(tǒng)時,雖然手機與服務器之間的連接可能會出現(xiàn)各種各樣的問題,不過可以將這些原因分成3種錯誤類別,分別是操作系統(tǒng)錯誤、 HTTP錯誤與應用錯誤。這些錯誤類別與創(chuàng)建HTTP請求的操作序列相關。圖5-2展示了向應用服務器發(fā)出的HTTP請求(提供來自于企業(yè)網(wǎng)絡的一些數(shù) 據(jù))的簡單序列圖。每塊陰影區(qū)域都表示這3種錯誤類型的錯誤域。典型地,操作系統(tǒng)錯誤是由HTTP服務器問題導致的。HTTP錯誤是由HTTP服務器或應 用服務器導致的。應用錯誤是由請求傳輸?shù)臄?shù)據(jù)或應用服務器查詢的其他系統(tǒng)導致的。
如果請求是安全的HTTPS請求,或是HTTP服務器被重定向客戶端,那么上面這個序列的步驟將會變得更加復雜。上述很多步驟都包含著大量的子步驟,比如在建立TCP連接時涉及的SYN與SYN-ACK包序列等。下面將會詳細介紹每一種錯誤類別。
5.1.1 操作系統(tǒng)錯誤
操作系統(tǒng)錯誤是由數(shù)據(jù)包沒有到達預定目標導致的。數(shù)據(jù)包可能是建立連接的一部分,也可能位于連接建立的中間階段。OS錯誤可能由如下原因造成:
沒有網(wǎng)絡——如果設備沒有數(shù)據(jù)網(wǎng)絡連接,那么連接嘗試很快就會被拒絕或是失敗。這些類型的錯誤可以通過Apple提供的Reachability框架檢測到,本節(jié)后面將會對此進行介紹。
無法路由到目標主機——設備可能有網(wǎng)絡連接,不過連接的目標可能位于隔離的網(wǎng)絡中或是處于離線狀態(tài)。這些錯誤有時可以由操作系統(tǒng)迅速檢測到,不過也有可能導致連接超時。
沒有應用監(jiān)聽目標端口——在請求到達目標主機后,數(shù)據(jù)包會被發(fā)送到請求指定的端口號。如果沒有服務器監(jiān)聽這個端口或是有太多的連接請求在排隊,那么連接請求就會被拒絕。
無法解析目標主機名——如果無法解析目標主機名,那么URL加載系統(tǒng)就會返回錯誤。通常情況下,這些錯誤是由配置錯誤或是嘗試訪問沒有外部名字解析且處于隔離網(wǎng)絡中的主機造成的。
在iOS的URL加載系統(tǒng)中,操作系統(tǒng)錯誤會以NSError對象的形式發(fā)送給應用。iOS通過NSError在軟件組件間傳遞錯誤信息。相比簡單的錯誤代碼來說,使用NSError的主要優(yōu)勢在于NSError對象包含了錯誤域屬性。
不過,NSError對象的使用并不限于操作系統(tǒng)。應用可以創(chuàng)建自己的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
- }
- }
域 屬性根據(jù)產(chǎn)生錯誤代碼的庫或框架對這些錯誤代碼進行隔離。借助域,框架開發(fā)者無須擔心覆蓋錯誤代碼,因為域屬性定義了產(chǎn)生錯誤的框架。比如,框架A 與B 都會產(chǎn)生錯誤代碼1,不過這兩個錯誤代碼會被每個框架提供的唯一域值進行區(qū)分。因此,如果代碼需要區(qū)分NSError 值,就必須對NSError 對象的code 與domain 屬性進行比較。
NSError 對象有如下3 個主要屬性:
code——標識錯誤的NSInteger 值。對于產(chǎn)生該錯誤的錯誤域來說,這個值是唯一的。
domain —— 指定錯誤域的NSString 指針, 比如NSPOSIXErrorDomain 、NSOSStatusErrorDomain 及NSMachErrorDomain。
userInfo——NSDictionary 指針,其中包含特定于錯誤的值。
URL 加載系統(tǒng)中產(chǎn)生的很多錯誤都來自于NSURLErrorDomain 域,代碼值基本上都來自于CFNetworkErrors.h 中定義的錯誤代碼。與iOS 提供的其他常量值一樣,代碼應該使用針對錯誤定義好的常量名而不是實際的錯誤代碼值。比如,如果客戶端無法連接到主機,那么錯誤代碼是1004,并且有定 義好的常量kCFURLErrorCannotConnectToHost。代碼絕不應該直接引用1004,因為這個值可能會在操作系統(tǒng)未來的修訂版中發(fā) 生變化;相反,應該使用提供的枚舉名kCFURLError。
如下是使用URL 加載系統(tǒng)創(chuàng)建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 的指針。如果出現(xiàn)錯誤,那么NSURLConnection對象只會實例化NSError 對象。URL 加載系統(tǒng)擁有NSError 對象;如果稍后代碼會用到它,那么應該保持這個對象。如果在同步請求完成后NSError 指針依然指向nil,那就說明沒有產(chǎn)生底層的OS 錯誤。這時,代碼就知道沒有產(chǎn)生OS 級別的錯誤,不過錯誤可能出現(xiàn)在協(xié)議棧的某個高層。
如果應用創(chuàng)建的是異步請求,那么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>
這是傳遞給請求委托的最終消息,委托必須能識別出錯誤的原因并作出恰當?shù)姆磻T谌缦率纠?,委托會向用戶展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>
上述代 碼以一種生硬且不友好的方式將錯誤展現(xiàn)給了用戶。在iOS 人機界面指南(HiG)中,Apple 建議不要過度使用UIAlertViews,因為這會破壞設備的使用感受。5.3 節(jié)“優(yōu)雅地處理網(wǎng)絡錯誤”中介紹了如何通過良好的用戶界面以一種干凈且一致的方式處理錯誤的模式。
iOS 設備通信錯誤的另一主要原因就是由于沒有網(wǎng)絡連接而導致設備無法訪問目標服務器??梢栽趪L試發(fā)起網(wǎng)絡連接前檢查一下網(wǎng)絡狀態(tài),這樣可以避免很多OS 錯誤。請記
住,這些設備可能會很快地進入或是離開網(wǎng)絡。因此,在每次調用前檢查網(wǎng)絡的可達性是非常合情合理的事情。
iOS 的SystemConfiguration 框架提供了多種方式來確定設備的網(wǎng)絡連接狀態(tài)??梢栽赟CNetworkReachability 參考文檔中找到關于底層API 的詳盡信息。這個API 非常強大,不過也有點隱秘。幸好,Apple 提供了一個名為Reachability 的示例程序,它為SCNetworkReachability實現(xiàn)了一個簡化、高層次的封裝器。Reachability 位于iOS 開發(fā)者庫中。
Reachability 封裝器提供如下4 個主要功能:
標識設備是否具備可用的網(wǎng)絡連接
標識當前的網(wǎng)絡連接是否可以到達某個特定的主機
標識當前使用的是哪種網(wǎng)絡技術:Wi-Fi、WWAN 還是什么技術都沒用
在網(wǎng)絡狀態(tài)發(fā)生變化時發(fā)出通知要想使用Reachability API,請從iOS 開發(fā)者庫中下載示例程序,地址是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,單擊+按鈕添加框架,這時會出現(xiàn)框架選擇界面。選擇SystemConfiguration 框架,單擊add 按鈕將其添加到項目中。
如下代碼片段會檢查是否存在網(wǎng)絡連接。不保證任何特定的主機或IP 地址是可達的,只是標識是否存在網(wǎng)絡連接。
- <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 元素或是當設備處于有限制的網(wǎng)絡中時修改超時值。如果應用需要知道當前正在使用的連接類型,那么請使用如下代碼:
- <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>
知道設備可達性狀態(tài)的變化也是很有必要的,這樣就可以主動修改應用行為。如下代碼片段啟動對網(wǎng)絡狀態(tài)的監(jiān)控:
- <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:的方法。當可達性狀態(tài)發(fā)生變化時,就向該對象傳遞NSNotification 及新的可達性狀態(tài)。如下示例展示了通知監(jiān)聽者:
- <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 網(wǎng)絡功能
- 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>
可達性還可以確定當前網(wǎng)絡上某個特定的主機是否是可達的。可以通過該特性根據(jù)應用是處于內部隔離的網(wǎng)絡上還是公開的Internet 上調整企業(yè)應用的行為。如下代碼示例展示了該特性:
- <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>
請記住,該特性對目標主機的訪問有個來回。如果每個請求都使用該特性,那就會極大增加應用的網(wǎng)絡負載與延遲。Apple 建議不要在主線程上檢測主機的可達性,因為嘗試訪問主機可能會阻塞主線程,這會導致UI 被凍結。
OS 錯誤首先就表明請求出現(xiàn)了問題。應用開發(fā)者有時會忽略掉它們,不過這樣做是有風險的。因為HTTP 使用了分層網(wǎng)絡,這時HTTP 層或是應用層可能會出現(xiàn)其他類型的潛在失敗情況。
#p#
5.1.2 HTTP 錯誤
HTTP 錯誤是由HTTP 請求、HTTP 服務器或應用服務器的問題造成的。HTTP 錯誤通過HTTP 響應的狀態(tài)碼發(fā)送給請求客戶端。
404 狀態(tài)是常見的一種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>
響應的***行有狀態(tài)碼。HTTP 響應可以帶有消息體,其中包含友好、用戶可讀的信息,用于描述發(fā)生的事情。你不應該將是否有響應體作為判斷HTTP 請求成功與否的標志。
一共有5 類HTTP 錯誤:
● 信息性質的100 級別——來自于HTTP 服務器的信息,表示請求的處理將會繼續(xù),不過帶有警告。
● 成功的200 級別——服務器處理了請求。每個200 級別的狀態(tài)都表示成功請求的不同結果。比如,204 表示請求成功,不過沒有向客戶端返回負載。
● 重定向需要的300 級別——表示客戶端必須執(zhí)行某個動作才能繼續(xù)請求,因為所需的資源已經(jīng)移動了。URL 加載系統(tǒng)的同步請求方法會自動處理重定向而無須通知代碼。如果應用需要對重定向進行自定義處理,那么應該使用異步請求。
● 客戶端錯誤400 級別——表示客戶端發(fā)出了服務器無法正確處理的錯誤數(shù)據(jù)。比如,未知的URL 或是不正確的HTTP 頭會導致這個范圍內的錯誤。
● 下游錯誤500 級別——表示HTTP 服務器與下游應用服務器之間出現(xiàn)了錯誤。比如,如果Web 服務器調用了JavaEE 應用服務器,Servlet 出現(xiàn)了NullPointerException,那么客戶端就會收到500 級別的錯誤。
iOS 中的URL 加載系統(tǒng)會處理HTTP 頭的解析,并可以輕松獲取到HTTP 狀態(tài)。如果代碼通過HTTP 或HTTPS URL 發(fā)出了同步調用,那么返回的響應對象就是一個NSHTTPURLResponse 實例。NSHTTPURLResponse 對象的statusCode 屬性會返回數(shù)值形式的請求的HTTP 狀態(tài)。如下代碼演示了對NSError 對象以及從HTTP 服務器返回的成功狀態(tài)的驗證:
- <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 狀態(tài)碼的權威信息,請參考W3 RFC 2616,網(wǎng)址是http://www.w3.org/Protocols/rfc2616/rfc2616.html。
5.1.3 應用錯誤
本節(jié)將會介紹網(wǎng)絡協(xié)議棧的下一層(應用層)產(chǎn)生的錯誤。應用錯誤不同于OS 錯誤或HTTP 錯誤,因為并沒有針對這些錯誤的標準值或是原因的集合。這些錯誤是由運行在服
務層之上的業(yè)務邏輯和應用造成的。在某些情況下,錯誤可能是代碼問題,比如異常,不過在其他一些情況下,錯誤可能是語義錯誤,比如向服務提供了無效的賬號等。對于前者來說,建議生成HTTP 500 級別的錯誤;對于后者來說,應該在應用負載中返回錯誤碼。
比如,如果用戶嘗試從賬戶中轉賬的金額超出了賬戶的可用余額,那么手機銀行就應該報告應用錯誤。如果發(fā)出了這樣的請求,那么OS 會說請求成功發(fā)送并接收到了響應。HTTP 服務器會報告接收到了請求并發(fā)出了響應,不過應用層必須報告這筆交易失敗。報告應用錯誤的***實踐是將應用的負載數(shù)據(jù)封裝在標準信封中,信封中含有一致的 應用錯誤位置信息。在上述資金轉賬示例中,成功的轉賬響應的業(yè)務負載應該如下所示:
- <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>
響 應包含了源賬號與目標賬號、轉賬的資金數(shù)額及確認號。直接將錯誤碼與錯誤消息放到transferResponse 對象中會導致錯誤碼與錯誤消息的定位變得困難。如果每個動作都將錯誤信息放到自己的響應對象中,就無法在應用間重用錯誤報告邏輯了。使用如下代碼中的數(shù)據(jù) 包結構可以讓應用快速確定是否出現(xiàn)了錯誤,方式是檢查響應的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 代碼是很容易重用的,因為錯誤信息總是位于響應負載的error 屬性中。此外,實際的交易負載處理得到了簡化,因為它總是位于相同的屬性名之下。
無論請求失敗的原因是什么,OS、HTTP 層還是應用,應用都必須能知道如何作出響應。你應該在開發(fā)時就提前考慮好應用所有的失敗模式,并設計好一致的方式來檢測并響應錯誤。
#p#
5.2 錯誤處理的經(jīng)驗法則
錯誤可能是由多種原因造成的,***處理方式也隨編寫的應用不同而不同。雖然很復雜,不過有一些經(jīng)驗法則可以幫助處理錯誤原因不可控的本質。
5.2.1 在接口契約中處理錯誤
在 設計服務接口時,只指定輸入、輸出與服務操作的做法是不正確的。接口契約還應該指定如何向客戶端發(fā)送錯誤信息。服務接口應該使用業(yè)界標準方式在可能的情況 下傳遞錯誤信息。比如,服務器不應該為服務端失敗定義新的HTTP 狀態(tài)值;相反,應該使用恰當?shù)?00 級別的狀態(tài)。如果使用了標準值,那么客戶端與服務端開發(fā)者就能對如何傳遞錯誤信息達成共識。應用絕不應該依賴于非標準的狀態(tài)或是其他屬性值來確定錯誤出現(xiàn) 與否。
應用開發(fā)者也不應該依賴于當前服務器軟件棧的行為來決定該如何處理錯誤。在部署了iOS 應用后,服務器軟件棧可能會由于未來的升級或替換而改變行為。
5.2.2 錯誤狀態(tài)可能不正確
移動網(wǎng)絡有如下有別于傳統(tǒng)Web 應用錯誤的不那么明顯的行為:模糊不清的錯誤報告。從移動設備發(fā)往服務器的任何網(wǎng)絡請求都有3 種可能的結果:
● 設備完全能夠確認操作是成功的。比如,NSError 與HTTP 狀態(tài)值都表明成功,返回的負載包含語義上正確的信息。
● 設備完全能夠確認操作是失敗的。比如,返回的應用負載包含來自于服務器的特定于本次操作的失敗標識。 設備模糊地確認操作是失敗的。比如,移動應用發(fā)出HTTP 請求以在兩個賬戶間轉賬。請求被銀行系統(tǒng)接收并正確地處理;然而,由于網(wǎng)絡失敗應答卻丟失了,NSURLConnection 報告超時。超時發(fā)生了,但卻是在轉賬請求成功之后發(fā)生的。如果重試該操作,那就會導致重復轉賬,可能還會造成賬戶透支。第3 種場景會導致應用出現(xiàn)意外和檢測不到的錯誤行為。如果應用開發(fā)者不知道第3種場景的存在,那么他們可能就會錯誤地假設操作失敗,然后不小心重試已經(jīng)成功的 操作。知道整個操作失敗還不夠,開發(fā)者必須考慮導致請求失敗的原因,以及自動重試每個失敗的請求是否是恰當?shù)摹?br />
5.2.3 驗證負載
應用 開發(fā)者不應該認為如果沒有OS 錯誤或HTTP 錯誤,負載就是有效的。在很多場景下,請求似乎是成功的,不過負載卻是無效的??蛻舳伺c服務器之間傳遞的負載都一種驗證機制。JSON 與XML 就是具備了驗證機制的負載格式,不過以逗號分隔的值文件與HTML 就沒有這種機制。
5.2.4 分離錯誤與正常的業(yè)務狀況
服務契約不應該將正常的業(yè)務狀況報告為錯誤。比如有個用戶,由于可能的欺詐導致賬戶被鎖定,鎖定狀態(tài)應該在數(shù)據(jù)負載中進行報告而不應該當作錯誤情況。分離錯誤與正常的業(yè)務狀況會讓代碼保持恰當?shù)年P注分離。只有當出現(xiàn)問題時才應該將之看成錯誤。
5.2.5 總是檢查HTTP 狀態(tài)
總是檢查HTTP 響應中的HTTP 狀態(tài),理解成功的狀態(tài)值,甚至向相同的服務發(fā)出重復的調用也是如此。服務器的狀態(tài)可能隨時會發(fā)生變化,甚至在并行的調用間也是如此。
5.2.6 總是檢查NSError 值
應用代碼應該總是檢查返回的NSError 值來確保OS 層沒有出現(xiàn)問題。即便知道應用總是運行在信號良好的Wi-Fi 網(wǎng)絡下也應該這樣做。任何東西都有出錯的可能性,代碼在處理網(wǎng)絡時也需要做好防御工作。
5.2.7 使用一致的方法來處理錯誤
網(wǎng) 絡錯誤的產(chǎn)生原因是非常多的,很難一一列舉出來,影響的多樣性及范圍也是非常大的。在設計應用時,請不要只關注于一致的用戶界面模式或是一致的命名模式。 你還應該設計一致的模式來處理網(wǎng)絡錯誤。該模式應該考慮到應用可能會遇到的所有類型的錯誤。如果應用的內部沒有以一致的模式處理這些錯誤,那么應用就無法 以一致的方式向用戶報告這些錯誤。
5.2.8 總是設置超時時間
在iOS 中,HTTP 請求的默認超時時間間隔是4 分鐘,這對于移動應用來說過長了,大多數(shù)用戶都不會在任何應用中等
iOS 簡化了網(wǎng)絡通信,不過對可能發(fā)生的所有類型的錯誤與邊界條件作出響應則不是那么輕松的事情。常見的做法是在網(wǎng)絡代碼中放置鉤子來快速查看結果,接下來再對 所有的錯誤情況進行處理。對于非移動應用來說,通常可以使用這種方式,因為來自工作站的網(wǎng)絡連接是可預測的。如果在應用加載時有網(wǎng)絡,那么當用戶加載下一 個頁面時基本上也會有網(wǎng)絡。絕大多數(shù)情況都是這樣的,開發(fā)者可以依賴瀏覽器向用戶顯示消息。如果在移動應用中沒有及時添加異常處理,那么當后面遇到新的錯 誤源時就需要大幅重構網(wǎng)絡代碼。
#p#
5.3 優(yōu)雅地處理網(wǎng)絡錯誤
本節(jié)將會介紹一種設計模式,用來創(chuàng)建一個優(yōu)雅且健壯的異常處理框架,并且在未來遇到新的錯誤時幾乎不需要做什么工作就能很好地進行擴展。考慮如下3 個移動通信中的主要異常情況:
● 由于設備沒有充分的網(wǎng)絡連接導致遠程服務器不可達。
● 由于OS 錯誤、HTTP 錯誤或是應用錯誤導致遠程服務器返回錯誤響應。
● 服務器需要認證,而設備嘗試發(fā)出未認證的請求。
隨著可能的異常數(shù)量呈現(xiàn)出線性增長,處理這些異常的代碼量則呈指數(shù)級增長。如果代碼要在每一類請求中處理所有這些錯誤,那么代碼的復雜性與數(shù)量就會呈指數(shù)級增長。本節(jié)將要介紹的模式會將這種指數(shù)級的曲線壓成線性曲線。
5.3.2 指揮調度模式示例
本節(jié)通過調用YouTube 的一項認證服務來介紹指揮調度模式。在此類通信過程中需要考慮很多失敗模式:
● 用戶可能沒有提供有效的身份信息。
● 設備可能無法聯(lián)網(wǎng)。
● YouTube 可能沒有及時響應或是出于某些原因失敗了。
應用需要以一種優(yōu)雅且可靠的方式處理每一種情況。該例將會闡述主要的代碼組件并介紹一些實現(xiàn)細節(jié)。項目中的應用是個示例應用,只用于演示目的。
1. 前提條件
要想成功運行該應用,你需要準備好如下內容:
● 一個YouTube 賬號。
● 至少向你的YouTube 賬號上傳一個視頻(無須公開,只要上傳到該賬號即可)。
● 從Wrox 網(wǎng)站上下載的項目壓縮文件。
該項目使用Xcode 4.1 與iOS 4.3 開發(fā),應用使用的是截止到2011 年10 月份的YouTubeAPI,不過該API 處于Google 的控制下,而且可能會發(fā)生變化。
2. 主要對象
下載好項目并在Xcode 中加載后,你會看到如下類:
1) 命令
命令分組中有如下一些類。
BaseCommand
BaseCommand 是所有命令對象的父類。它提供了每個命令類所需的眾多方法,這些方法有:
發(fā)送完成、錯誤與登錄通知的方法。
用于讓對象監(jiān)聽完成通知的方法。
用于支持實際的NSURLRequests 的方法。
BaseCommand 繼承了NSOperation,因此所有的命令邏輯都位于該類的每個子類對象的main 方法中。
GetFeed
如 代碼清單5-1 所示,該類的main 方法會調用YouTube 并加載當前登錄用戶上傳的視頻列表。YouTube 通過請求HTTP 頭中的令牌來確定登錄用戶的身份。如果沒有這個頭,YouTube 就會返回HTTP 狀態(tài)碼0 而不是更加標準的4xx HTTP 錯誤。
代碼清單5-1 CommandDispathDemo/service-interface/GetFeed.h
- <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>
在 上述代碼清單中,通過self 調用的很多方法都是在BaseCommand 父類中實現(xiàn)的。GetFeed 命令就是指揮調度模式的原型。main 方法會對用戶登錄進行檢查,因為如果這個調用失敗了,那就沒必要再調用服務器了。如果用戶已經(jīng)登錄,那么代碼就會構建請求,將認證頭添加到請求中,然后發(fā) 送一條同步請求。代碼的***一部分會調用一個父類方法來確定調用是否成功。該方法使用來自于NSHTTPURLResponse 對象的NSError 對象與HTTP 狀態(tài)碼來確定是否成功。如果調用失敗,就會廣播一條錯誤通知或是需要登錄的通知。
LoginCommand
該命令會向YouTube 發(fā)出對用戶進行認證的請求。該命令比較獨立,因為并沒有使用BaseCommand 對象的輔助方法。之所以沒有使用這些方法,是因為如果登錄失敗,就不應該生成需要認證的失敗消息,而只會報告正常完成或是失敗的狀態(tài)。
登 錄監(jiān)聽器會處理來自于登錄失敗的錯誤。要想了解關于YouTube 所需協(xié)議的詳細信息,請參考 http://code.google.com/apis/youtube/2.0 /developers_guide_protocol_understanding_video_feeds.html。
2) 異常監(jiān)聽器
監(jiān) 聽器分組中有視圖控制器, 當錯誤發(fā)生或是用戶需要登錄時會呈現(xiàn)出來。NetworkErrorViewController 與LoginViewController 都繼承了InterstitialViewController,后者提供了幾個常用的輔助方法。這兩個視圖控制器都會以模態(tài)視圖控制器的形式呈現(xiàn)出來。
● NetworkErrorViewController:向用戶提供重試或是放棄失敗操作的選擇。如果用戶選擇重試,那么失敗命令就會放回到操作隊列中。
● LoginViewController:向用戶請求用戶名與密碼。位于視圖棧的頂部,直到用戶成功登錄為止。 InterstitialViewController:作為其他異常監(jiān)聽器的父監(jiān)聽器,提供了一些支持功能,比如收集多個錯誤通知以及當錯誤解析完畢時 重新分發(fā)錯誤的代碼等。監(jiān)聽器的關鍵代碼位于viewDidDisappear:方法中(如代碼清單5-2 所示),當視圖完全消失時會調用該方法。如果在視圖完全消失前命令已進入隊列中,那么其他錯誤就有可能導致再一次呈現(xiàn)視圖,這會導致應用出現(xiàn)嚴重的錯誤。 iOS 5 提供了處理這個問題的更好方式,因為在視圖消失時用戶可以指定執(zhí)行的代碼塊。在處理觸發(fā)命令前,代碼并不需要確定消失的原因。
代碼清單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>
應用委托會將自身注冊為網(wǎng)絡錯誤與需要登錄通知的監(jiān)聽器(如代碼清單5-3 所示),收集異常通知并在錯誤發(fā)生時管理正確的視圖控制器的呈現(xiàn)。上述代碼展示了需要登錄通知的通知處理器。由于要處理用戶界面,因此其中的內容必須使用GCD 在主線程中執(zhí)行。
代碼清單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。當 該控制器加載時,會創(chuàng)建并排隊命令以加載用戶的視頻列表(又叫做YouTube 種子),并且會將控制流放回到主運行循環(huán)中以耐心等待命令的完成。
***次調用總是失敗的,因為這時用戶還沒有登錄。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>
注意,代碼并不需要檢查用戶是否已經(jīng)登錄;在執(zhí)行時命令會做檢查。
gotFeed:方法會處理來自于YouTube 的最終返回數(shù)據(jù)。在此例中,requestVideoFeed:方法會將gotFeed:方法注冊為完成通知的目標方法。如果調用成功,該方法會將數(shù)據(jù)加載
到表視圖中,否則顯示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>
這個類會改變完成通知名,這樣它(也只有它)就可以接收到特定圖片的通知了。否則,它還需要檢查返回的通知來確定是否是之前發(fā)出的命令。
指 揮調度模式的優(yōu)雅之處在于能將應用中所有凌亂的異常處理邏輯和登錄呈現(xiàn)邏輯與主視圖控制器分離開來。當視圖控制器發(fā)出命令時,會忽略掉所有的異常處理與認 證處理,只是完成請求而已。只是發(fā)出請求,等待響應,然后處理響應。并不關心用戶注冊的請求是不是重試了5 次才成功。此外,服務請求代碼并不需要知道請求來自于哪里,結果去向哪里;只是關注于執(zhí)行調用并廣播結果。
指揮調度模式還有其他優(yōu)勢,開發(fā)者一開始會編寫一些代碼并論證結果,如果順利,那么會添加異常處理器,而這對之前的代碼不會造成任何影響。此外,如果設計恰當,那么所有的網(wǎng)絡服務調用都會使用相同的基礎命令類,這會減少命令類的數(shù)量。
在通用應用中,可以通過異常監(jiān)聽器調整展示的視圖,這樣iPhone 上的錯誤顯示界面就會適配于該平臺,iPad 上的錯誤顯示界面也會適配于更大的平臺。
這種模式可以快速展示結果,對業(yè)務邏輯與異常處理進行關注分離,減少重復代碼以及提供更好的用戶體驗。
5.4 小結
代碼使用網(wǎng)絡時會出現(xiàn)很多錯誤源,理解錯誤源有助于快速診斷并解析網(wǎng)絡問題。借助于Reachability 框架,代碼
本文鏈接:http://my.oschina.net/louiseliu/blog/289253