本章內容html
● iOS應用中的網絡錯誤源ios
● 檢測網絡的可達性程序員
● 錯誤處理的經驗法則設計模式
● 處理網絡錯誤的設計模式api
到目前爲止,咱們所介紹的iPhone與其餘系統的網絡交互都是基於一切正常這個假設。本章將會放棄這個假設,並深刻探究網絡的真實世界。在真實世界中,事情是會出錯的,有時多是很是嚴重的錯誤:手機進入與離開網絡、包丟掉或是延遲;網絡基礎設施出錯;偶爾用戶還會出錯。若是一切正常,那麼編寫iOS應用就會簡單很多,不過遺憾的是現實並不是如此。本章將會探討致使網絡操做失敗的幾個因素,介紹系統如何將失敗狀況告知應用,應用又該如何優雅地通知用戶。此外,本章還將介紹如何在不往應用邏輯中添加錯誤處理代碼的狀況下,以一種整潔且一致的方式處理錯誤的軟件模式。瀏覽器
早期的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包序列等。下面將會詳細介紹每一種錯誤類別。
操做系統錯誤是由數據包沒有到達預約目標緻使的。數據包多是創建鏈接的一部分,也可能位於鏈接創建的中間階段。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 請求的代碼示例:
<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 設備通訊錯誤的另外一主要緣由就是因爲沒有網絡鏈接而致使設備沒法訪問目標服務器。能夠在嘗試發起網絡鏈接前檢查一下網絡狀態,這樣能夠避免不少OS 錯誤。請記
住,這些設備可能會很快地進入或是離開網絡。所以,在每次調用前檢查網絡的可達性是很是合情合理的事情。
iOS 的SystemConfiguration 框架提供了多種方式來肯定設備的網絡鏈接狀態。能夠在SCNetworkReachability 參考文檔中找到關於底層API 的詳盡信息。這個API 很是強大,不過也有點隱祕。幸虧,Apple 提供了一個名爲Reachability 的示例程序,它爲SCNetworkReachability實現了一個簡化、高層次的封裝器。Reachability 位於iOS 開發者庫中。
Reachability 封裝器提供以下4 個主要功能:
● 標識設備是否具有可用的網絡鏈接
● 標識當前的網絡鏈接是否能夠到達某個特定的主機
● 標識當前使用的是哪一種網絡技術:Wi-Fi、WWAN 仍是什麼技術都沒用
● 在網絡狀態發生變化時發出通知要想使用Reachability API,請從iOS 開發者庫中下載示例程序,地址是http://developer.apple.com/library/ios/#samplecode/Reachability/Introduction/Intro.html,而後將Reachability.h與Reachability.m 添加到應用的Xcode 項目中。此外,還須要將SystemConfiguration 框架添加到Xcode 項目中。將SystemConfiguration 框架添加到Xcode 項目中須要編輯項目配置。圖5-3 展現了將SystemConfiguration 框架添到Xcode 項目中所需的步驟。
(3) 選擇SystemConfiguration.framework
選定好項目目標後,找到設置中的Linked Frameworks and Libraries,單擊+按鈕添加框架,這時會出現框架選擇界面。選擇SystemConfiguration 框架,單擊add 按鈕將其添加到項目中。
以下代碼片斷會檢查是否存在網絡鏈接。不保證任何特定的主機或IP 地址是可達的,只是標識是否存在網絡鏈接。
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="font-family:Microsoft YaHei;font-size:14px;">#import "Reachability.h"
...
if([[Reachability reachabilityForInternetConnection]
currentReachabilityStatus] == NotReachable) {
// handle the lack of a network
}</span></span>
在某些狀況下,你可能想要修改某些動做、禁用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 服務器或應用服務器的問題形成的。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>
響應的第一行有狀態碼。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>
若是請求的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 服務器會報告接收到了請求併發出了響應,不過應用層必須報告這筆交易失敗。報告應用錯誤的最佳實踐是將應用的負載數據封裝在標準信封中,信封中含有一致的應用錯誤位置信息。在上述資金轉帳示例中,成功的轉帳響應的業務負載應該以下所示:
<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 代碼是很容易重用的,由於錯誤信息老是位於響應負載的error 屬性中。此外,實際的交易負載處理獲得了簡化,由於它老是位於相同的屬性名之下。
不管請求失敗的緣由是什麼,OS、HTTP 層仍是應用,應用都必須能知道如何做出響應。你應該在開發時就提早考慮好應用全部的失敗模式,並設計好一致的方式來檢測並響應錯誤。
5.2 錯誤處理的經驗法則
錯誤多是由多種緣由形成的,最佳處理方式也隨編寫的應用不一樣而不一樣。雖然很複雜,不過有一些經驗法則能夠幫助處理錯誤緣由不可控的本質。
5.2.1 在接口契約中處理錯誤
在設計服務接口時,只指定輸入、輸出與服務操做的作法是不正確的。接口契約還應該指定如何向客戶端發送錯誤信息。服務接口應該使用業界標準方式在可能的狀況下傳遞錯誤信息。好比,服務器不該該爲服務端失敗定義新的HTTP 狀態值;相反,應該使用恰當的500 級別的狀態。若是使用了標準值,那麼客戶端與服務端開發者就能對如何傳遞錯誤信息達成共識。應用毫不應該依賴於非標準的狀態或是其餘屬性值來肯定錯誤出現與否。
應用開發者也不該該依賴於當前服務器軟件棧的行爲來決定該如何處理錯誤。在部署了iOS 應用後,服務器軟件棧可能會因爲將來的升級或替換而改變行爲。
5.2.2 錯誤狀態可能不正確
移動網絡有以下有別於傳統Web 應用錯誤的不那麼明顯的行爲:模糊不清的錯誤報告。從移動設備發往服務器的任何網絡請求都有3 種可能的結果:
● 設備徹底可以確認操做是成功的。好比,NSError 與HTTP 狀態值都代表成功,返回的負載包含語義上正確的信息。
● 設備徹底可以確認操做是失敗的。好比,返回的應用負載包含來自於服務器的特定於本次操做的失敗標識。
● 設備模糊地確認操做是失敗的。好比,移動應用發出HTTP 請求以在兩個帳戶間轉帳。請求被銀行系統接收並正確地處理;然而,因爲網絡失敗應答卻丟失了,NSURLConnection 報告超時。超時發生了,但倒是在轉帳請求成功以後發生的。若是重試該操做,那就會致使重複轉帳,可能還會形成帳戶透支。第3 種場景會致使應用出現意外和檢測不到的錯誤行爲。若是應用開發者不知道第3種場景的存在,那麼他們可能就會錯誤地假設操做失敗,而後不當心重試已經成功的操做。知道整個操做失敗還不夠,開發者必須考慮致使請求失敗的緣由,以及自動重試每一個失敗的請求是不是恰當的。
5.2.3 驗證負載
應用開發者不該該認爲若是沒有OS 錯誤或HTTP 錯誤,負載就是有效的。在不少場景下,請求彷佛是成功的,不過負載倒是無效的。客戶端與服務器之間傳遞的負載都一種驗證機制。JSON 與XML 就是具有了驗證機制的負載格式,不過以逗號分隔的值文件與HTML 就沒有這種機制。
5.2.4 分離錯誤與正常的業務情況
服務契約不該該將正常的業務情況報告爲錯誤。好比有個用戶,因爲可能的欺詐致使帳戶被鎖定,鎖定狀態應該在數據負載中進行報告而不該該看成錯誤狀況。分離錯誤與正常的業務情況會讓代碼保持恰當的關注分離。只有當出現問題時才應該將之當作錯誤。
5.2.5 老是檢查HTTP 狀態
老是檢查HTTP 響應中的HTTP 狀態,理解成功的狀態值,甚至向相同的服務發出重複的調用也是如此。服務器的狀態可能隨時會發生變化,甚至在並行的調用間也是如此。
5.2.6 老是檢查NSError 值
應用代碼應該老是檢查返回的NSError 值來確保OS 層沒有出現問題。即使知道應用老是運行在信號良好的Wi-Fi 網絡下也應該這樣作。任何東西都有出錯的可能性,代碼在處理網絡時也須要作好防護工做。
5.2.7 使用一致的方法來處理錯誤
網絡錯誤的產生緣由是很是多的,很難一一列舉出來,影響的多樣性及範圍也是很是大的。在設計應用時,請不要只關注於一致的用戶界面模式或是一致的命名模式。你還應該設計一致的模式來處理網絡錯誤。該模式應該考慮到應用可能會遇到的全部類型的錯誤。若是應用的內部沒有以一致的模式處理這些錯誤,那麼應用就沒法以一致的方式向用戶報告這些錯誤。
5.2.8 老是設置超時時間
在iOS 中,HTTP 請求的默認超時時間間隔是4 分鐘,這對於移動應用來講過長了,大多數用戶都不會在任何應用中等待4 分鐘。開發者須要選擇合理的超時時間,方式是評估網絡請求的可能響應時間,而後將最差的網絡場景下的網絡延遲考慮進去。以下示例展現瞭如何建立具備20 秒超時時間的請求:
<span style="font-family:Microsoft YaHei;font-size:14px;">- (NSMutableURLRequest *) createRequestObject:(NSURL *)url {
NSMutableURLRequest *request = [[[NSMutableURLRequestalloc]
initWithURL:url
cachePolicy:NSURLCacheStorageAllowed
timeoutInterval:20
autorelease];
return request;
}</span>
5.3 優雅地處理網絡錯誤
iOS 簡化了網絡通訊,不過對可能發生的全部類型的錯誤與邊界條件做出響應則不是那麼輕鬆的事情。常見的作法是在網絡代碼中放置鉤子來快速查看結果,接下來再對全部的錯誤狀況進行處理。對於非移動應用來講,一般可使用這種方式,由於來自工做站的網絡鏈接是可預測的。若是在應用加載時有網絡,那麼當用戶加載下一個頁面時基本上也會有網絡。絕大多數狀況都是這樣的,開發者能夠依賴瀏覽器向用戶顯示消息。若是在移動應用中沒有及時添加異常處理,那麼當後面遇到新的錯誤源時就須要大幅重構網絡代碼。
本節將會介紹一種設計模式,用來建立一個優雅且健壯的異常處理框架,而且在將來遇到新的錯誤時幾乎不須要作什麼工做就能很好地進行擴展。考慮以下3 個移動通訊中的主要異常狀況:
● 因爲設備沒有充分的網絡鏈接致使遠程服務器不可達。
● 因爲OS 錯誤、HTTP 錯誤或是應用錯誤致使遠程服務器返回錯誤響應。
● 服務器須要認證,而設備嘗試發出未認證的請求。
隨着可能的異常數量呈現出線性增加,處理這些異常的代碼量則呈指數級增加。若是代碼要在每一類請求中處理全部這些錯誤,那麼代碼的複雜性與數量就會呈指數級增加。本節將要介紹的模式會將這種指數級的曲線壓成線性曲線。
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 方法會對用戶登陸進行檢查,由於若是這個調用失敗了,那就不必再調用服務器了。若是用戶已經登陸,那麼代碼就會構建請求,將認證頭添加到請求中,而後發送一條同步請求。代碼的最後一部分會調用一個父類方法來肯定調用是否成功。該方法使用來自於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>
應用委託會將自身註冊爲網絡錯誤與須要登陸通知的監聽器(如代碼清單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 框架,代碼能夠主動對變化的網絡情況做出響應,從而避免沒必要要的網絡錯誤的出現。在發出網絡請求以及處理成功與失敗的結果時遵循一致的模式,能夠確保代碼更加整潔、更加具備可維護性。
微信:qinghuashuyou
更多最新圖書請點擊查看哦