程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> PHP編程 >> 關於PHP編程 >> iOS網絡高級編程:iPhone和iPad的企業應用開發之錯誤處理

iOS網絡高級編程:iPhone和iPad的企業應用開發之錯誤處理

編輯:關於PHP編程

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

域屬性根據產生錯誤代碼的庫或框架對這些錯誤代碼進行隔離。借助域,框架開發者無須擔心覆蓋錯誤代碼,因為域屬性定義了產生錯誤的框架。比如,框架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 請求的代碼示例:

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
}

注意,NSError 對象被聲明為指向nil 的指針。如果出現錯誤,那麼NSURLConnection對象只會實例化NSError 對象。URL 加載系統擁有NSError 對象;如果稍後代碼會用到它,那麼應該保持這個對象。如果在同步請求完成後NSError 指針依然指向nil,那就說明沒有產生底層的OS 錯誤。這時,代碼就知道沒有產生OS 級別的錯誤,不過錯誤可能出現在協議棧的某個高層。
如果應用創建的是異步請求,那麼NSError 對象就會返回到委托類的下面這個方法:

- (void)connection:(NSURLConnection *)connection
didFailWithError:(NSError *)error
這是傳遞給請求委托的最終消息,委托必須能識別出錯誤的原因並作出恰當的反應。在如下示例中,委托會向用戶展UIAlertView:
- (void) connection:conndidFailWithError:error {
UIAlertView *alert = [UIAlertViewalloc] initWithTitle:@"Network Error"
message:[error description]
delegate:self
cancelButtonTitle:@"Oh Well"
otherButtonTitles:nil];
[alert show];
[alert release];
}

上述代碼以一種生硬且不友好的方式將錯誤展現給了用戶。在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 地址是可達的,只是標識是否存在網絡連接。

#import "Reachability.h"
...
if([[Reachability reachabilityForInternetConnection]
currentReachabilityStatus] == NotReachable) {
// handle the lack of a network
}

在某些情況下,你可能想要修改某些動作、禁用UI 元素或是當設備處於有限制的網絡中時修改超時值。如果應用需要知道當前正在使用的連接類型,那麼請使用如下代碼:

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

知道設備可達性狀態的變化也是很有必要的,這樣就可以主動修改應用行為。如下代碼片段啟動對網絡狀態的監控:

#import "Reachability.h"
...
[[NSNotificationCenterdefaultCenter]
addObserver:self
selector:@selector(networkChanged:)
name:kReachabilityChangedNotification
object:nil];
Reachability *reachability;
reachability = [[Reachability reachabilityForInternetConnection] retain];
[reachability startNotifier];

上述代碼將當前對象注冊為通知觀察者,名為kReachabilityChangedNotification。
NSNotificationCenter 會調用當前對象的名為networkChanged:的方法。當可達性狀態發生變化時,就向該對象傳遞NSNotification 及新的可達性狀態。如下示例展示了通知監聽者:

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

可達性還可以確定當前網絡上某個特定的主機是否是可達的。可以通過該特性根據應用是處於內部隔離的網絡上還是公開的Internet 上調整企業應用的行為。如下代碼示例展示了該特性:

Reachability *reach = [Reachability
reachabilityWithHostName:@"www.captechconsulting.com"];
if(reachability == NotReachable) {
// The target host is not reachable available
}

請記住,該特性對目標主機的訪問有個來回。如果每個請求都使用該特性,那就會極大增加應用的網絡負載與延遲。Apple 建議不要在主線程上檢測主機的可達性,因為嘗試訪問主機可能會阻塞主線程,這會導致UI 被凍結。
OS 錯誤首先就表明請求出現了問題。應用開發者有時會忽略掉它們,不過這樣做是有風險的。因為HTTP 使用了分層網絡,這時HTTP 層或是應用層可能會出現其他類型的潛在失敗情況。

5.1.2 HTTP 錯誤
HTTP 錯誤是由HTTP 請求、HTTP 服務器或應用服務器的問題造成的。HTTP 錯誤通過HTTP 響應的狀態碼發送給請求客戶端。
404 狀態是常見的一種HTTP 錯誤,表示找不到URL 指定的資源。下述代碼片段中的HTTP 頭就是當HTTP 服務器找不到請求資源時給出的原始輸出:

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

響應的第一行有狀態碼。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 服務器返回的成功狀態的驗證:

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

如果請求的URL不是HTTP,那麼應用就應該驗證響應對象是否是NSHTTPURLResponse對象。驗證對象類型的首選方法是使用返回對象的isKindOfClass:方法,如下所示:

if([response isKindOfClass:[NSHTTPURLResponse class]]) {
// It is a HTTP response, so we can check the status code
...
要想了解關於HTTP 狀態碼的權威信息,請參考W3 RFC 2616,網址是http://www.w3.org/Protocols/rfc2616/rfc2616.html。
5.1.3 應用錯誤
本節將會介紹網絡協議棧的下一層(應用層)產生的錯誤。應用錯誤不同於OS 錯誤或HTTP 錯誤,因為並沒有針對這些錯誤的標准值或是原因的集合。這些錯誤是由運行在服
務層之上的業務邏輯和應用造成的。在某些情況下,錯誤可能是代碼問題,比如異常,不過在其他一些情況下,錯誤可能是語義錯誤,比如向服務提供了無效的賬號等。對於前者來說,建議生成HTTP 500 級別的錯誤;對於後者來說,應該在應用負載中返回錯誤碼。
比如,如果用戶嘗試從賬戶中轉賬的金額超出了賬戶的可用余額,那麼手機銀行就應該報告應用錯誤。如果發出了這樣的請求,那麼OS 會說請求成功發送並接收到了響應。HTTP 服務器會報告接收到了請求並發出了響應,不過應用層必須報告這筆交易失敗。報告應用錯誤的最佳實踐是將應用的負載數據封裝在標准信封中,信封中含有一致的應用錯誤位置信息。在上述資金轉賬示例中,成功的轉賬響應的業務負載應該如下所示:

{ "transferResponse":{
"fromAccount":1,
"toAccount":5,
"amount":500.00,
"confirmation":232348844
}
}


響應包含了源賬號與目標賬號、轉賬的資金數額及確認號。直接將錯誤碼與錯誤消息放到transferResponse 對象中會導致錯誤碼與錯誤消息的定位變得困難。如果每個動作都將錯誤信息放到自己的響應對象中,就無法在應用間重用錯誤報告邏輯了。使用如下代碼中的數據包結構可以讓應用快速確定是否出現了錯誤,方式是檢查響應的JSON 負載中是否存在“error”對象:


{"error":{
"code":900005,
"messages":"Insufficient Funds to Complete Transfer"
},
"data":{
"fromAccount":1,
"toAccount":5,
"amount":500.00
}
}


報告錯誤的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 秒超時時間的請求:
- (NSMutableURLRequest *) createRequestObject:(NSURL *)url {
NSMutableURLRequest *request = [[[NSMutableURLRequestalloc]
initWithURL:url
cachePolicy:NSURLCacheStorageAllowed
timeoutInterval:20
autorelease];
return request;
}

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
- (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];
}
}
}

在上述代碼清單中,通過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
- (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;
}
應用委托會將自身注冊為網絡錯誤與需要登錄通知的監聽器(如代碼清單5-3 所示),收集異常通知並在錯誤發生時管理正確的視圖控制器的呈現。上述代碼展示了需要登錄通知的通知處理器。由於要處理用戶界面,因此其中的內容必須使用GCD 在主線程中執行。
代碼清單5-3 CommandDispatchDemo/CommandDispatchDemoAppDelegate.m
/**
* 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
}

3) 視圖控制器
在這個簡單的應用中有個主要的視圖控制器。RootViewController(參見下面的代碼)繼承了UITableViewController。當該控制器加載時,會創建並排隊命令以加載用戶的視頻列表(又叫做YouTube 種子),並且會將控制流放回到主運行循環中以耐心等待命令的完成。
第一次調用總是失敗的,因為這時用戶還沒有登錄。CommandDispatchDemo/RootViewController.m 的requestVideoFeed 方法會啟動加載視頻列表的過程,如下所示:
(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
[op listenForMyCompletion:self selector:@selector(gotFeed:)];
// put it on the queue for execution
[op enqueueOperation];
[op release];
}

注意,代碼並不需要檢查用戶是否已經登錄;在執行時命令會做檢查。
gotFeed:方法會處理來自於YouTube 的最終返回數據。在此例中,requestVideoFeed:方法會將gotFeed:方法注冊為完成通知的目標方法。如果調用成功,該方法會將數據加載
到表視圖中,否則顯示UIAlertView:
- (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];
}
這個類會改變完成通知名,這樣它(也只有它)就可以接收到特定圖片的通知了。否則,它還需要檢查返回的通知來確定是否是之前發出的命令。
指揮調度模式的優雅之處在於能將應用中所有凌亂的異常處理邏輯和登錄呈現邏輯與主視圖控制器分離開來。當視圖控制器發出命令時,會忽略掉所有的異常處理與認證處理,只是完成請求而已。只是發出請求,等待響應,然後處理響應。並不關心用戶注冊的請求是不是重試了5 次才成功。此外,服務請求代碼並不需要知道請求來自於哪裡,結果去向哪裡;只是關注於執行調用並廣播結果。 指揮調度模式還有其他優勢,開發者一開始會編寫一些代碼並論證結果,如果順利,那麼會添加異常處理器,而這對之前的代碼不會造成任何影響。此外,如果設計恰當,那麼所有的網絡服務調用都會使用相同的基礎命令類,這會減少命令類的數量。 在通用應用中,可以通過異常監聽器調整展示的視圖,這樣iPhone 上的錯誤顯示界面就會適配於該平台,iPad 上的錯誤顯示界面也會適配於更大的平台。 這種模式可以快速展示結果,對業務邏輯與異常處理進行關注分離,減少重復代碼以及提供更好的用戶體驗。

5.4 小結
代碼使用網絡時會出現很多錯誤源,理解錯誤源有助於快速診斷並解析網絡問題。借助於Reachability 框架,代碼可以主動對變化的網絡狀況作出響應,從而避免不必要的網絡錯誤的出現。在發出網絡請求以及處理成功與失敗的結果時遵循一致的模式,可以確保代碼更加整潔、更加具有可維護性。

《iOS網絡高級編程:iPhone和iPad的企業應用開發》試讀電子書免費提供,有需要的留下郵箱,一有空即發送給大家。 別忘啦頂哦!
微信:qinghuashuyou 更多最新圖書請點擊查看哦 \


更多0
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved