1. 程式人生 > >iOS 7從 NSURLConnection 到 NSURLSession

iOS 7從 NSURLConnection 到 NSURLSession

iOS 7 和 Mac OS X 10.9 Mavericks 中一個顯著的變化就是對 Foundation URL 載入系統的徹底重構。

現在已經有人在深入蘋果的網路層基礎架構的地方做研究了,所以我想是時候來分享一些對於我對於這些新的 API 的看法和心得了,新的 API 將如何影響我們編寫程式,以及它們對於 API 設計理念的影響。

NSURLConnection 作為 Core Foundation / CFNetwork 框架的 API 之上的一個抽象,在 2003 年,隨著第一版的 Safari 的釋出就釋出了。NSURLConnection 這個名字,實際上是指代的 Foundation 框架的 URL 載入系統中一系列有關聯的元件:NSURLRequest

NSURLResponseNSURLProtocol、 NSURLCache、 NSHTTPCookieStorageNSURLCredentialStorage 以及同名類 NSURLConnection

NSURLRequest 被傳遞給 NSURLConnection。被委託物件(遵守以前的非正式協議 <NSURLConnectionDelegate> 和 <NSURLConnectionDataDelegate>)非同步地返回一個 NSURLResponse 以及包含伺服器返回資訊的 NSData

在一個請求被髮送到伺服器之前,系統會先查詢共享的快取資訊,然後根據策略(policy)

以及可用性(availability)的不同,一個已經被快取的響應可能會被立即返回。如果沒有快取的響應可用,則這個請求將根據我們指定的策略來快取它的響應以便將來的請求可以使用。

在把請求傳送給伺服器的過程中,伺服器可能會發出鑑權查詢(authentication challenge),這可以由共享的 cookie 或機密儲存(credential storage)來自動響應,或者由被委託物件來響應。傳送中的請求也可以被註冊的 NSURLProtocol 物件所攔截,以便在必要的時候無縫地改變其載入行為。

不管怎樣,NSURLConnection 作為網路基礎架構,已經服務了成千上萬的 iOS 和 Mac OS 程式,並且做的還算相當不錯。但是這些年,一些用例——尤其是在 iPhone 和 iPad 上面——已經對 NSURLConnection

 的幾個核心概念提出了挑戰,讓蘋果有理由對它進行重構。

在 2013 的 WWDC 上,蘋果推出了 NSURLConnection 的繼任者:NSURLSession

和 NSURLConnection 一樣,NSURLSession 指的也不僅是同名類 NSURLSession,還包括一系列相互關聯的類。NSURLSession 包括了與之前相同的元件,NSURLRequest 與 NSURLCache,但是把 NSURLConnection 替換成了 NSURLSessionNSURLSessionConfiguration 以及 NSURLSessionTask 的 3 個子類:NSURLSessionDataTaskNSURLSessionUploadTaskNSURLSessionDownloadTask

與 NSURLConnection 相比,NSURLsession 最直接的改進就是可以配置每個 session 的快取,協議,cookie,以及證書策略(credential policy),甚至跨程式共享這些資訊。這將允許程式和網路基礎框架之間相互獨立,不會發生干擾。每個 NSURLSession 物件都由一個 NSURLSessionConfiguration 物件來進行初始化,後者指定了剛才提到的那些策略以及一些用來增強移動裝置上效能的新選項。

NSURLSession 中另一大塊就是 session task。它負責處理資料的載入以及檔案和資料在客戶端與服務端之間的上傳和下載。NSURLSessionTask 與 NSURLConnection 最大的相似之處在於它也負責資料的載入,最大的不同之處在於所有的 task 共享其創造者 NSURLSession 這一公共委託者(common delegate)

我們先來深入探討 task,過後再來討論 NSURLSessionConfiguration

NSURLSessionTask

NSURLsessionTask 是一個抽象類,其下有 3 個實體子類可以直接使用:NSURLSessionDataTaskNSURLSessionUploadTaskNSURLSessionDownloadTask。這 3 個子類封裝了現代程式三個最基本的網路任務:獲取資料,比如 JSON 或者 XML,上傳檔案和下載檔案。

NSURLSessionTask class diagram

當一個 NSURLSessionDataTask 完成時,它會帶有相關聯的資料,而一個 NSURLSessionDownloadTask 任務結束時,它會帶回已下載檔案的一個臨時的檔案路徑。因為一般來說,服務端對於一個上傳任務的響應也會有相關資料返回,所以 NSURLSessionUploadTask 繼承自 NSURLSessionDataTask

所有的 task 都是可以取消,暫停或者恢復的。當一個 download task 取消時,可以通過選項來建立一個恢復資料(resume data),然後可以傳遞給下一次新建立的 download task,以便繼續之前的下載。

不同於直接使用 alloc-init 初始化方法,task 是由一個 NSURLSession 建立的。每個 task 的構造方法都對應有或者沒有 completionHandler 這個 block 的兩個版本,例如:有這樣兩個構造方法 –dataTaskWithRequest: 和 –dataTaskWithRequest:completionHandler:。這與 NSURLConnection 的 -sendAsynchronousRequest:queue:completionHandler: 方法類似,通過指定 completionHandler 這個 block 將建立一個隱式的 delegate,來替代該 task 原來的 delegate——session。對於需要 override 原有 session task 的 delegate 的預設行為的情況,我們需要使用這種不帶 completionHandler 的版本。

NSURLSessionTask 的工廠方法

在 iOS 5 中,NSURLConnection 添加了 sendAsynchronousRequest:queue:completionHandler: 這一方法,對於一次性使用的 request, 大大地簡化程式碼,同時它也是 sendSynchronousRequest:returningResponse:error: 這個方法的非同步替代品:

 NSURL *URL = [NSURL URLWithString:@"http://example.com"];
 NSURLRequest *request = [NSURLRequest requestWithURL:URL];

 [NSURLConnection sendAsynchronousRequest:request
                                    queue:[NSOperationQueue mainQueue]
                        completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
     // ...
 }];

NSURLSession 在 task 的構造方法上延續了這一模式。不同的是,這裡不會立即執行 task,而是將該 task 物件先返回,允許我們進一步的配置,然後可以使用 resume 方法來讓它開始執行。

Data task 可以通過 NSURL 或 NSURLRequest 建立(使用前者相當於是使用一個對於該 URL 進行標準 GET 請求的 NSURLRequest,這是一種快捷方法):

 NSURL *URL = [NSURL URLWithString:@"http://example.com"];
 NSURLRequest *request = [NSURLRequest requestWithURL:URL];

 NSURLSession *session = [NSURLSession sharedSession];
 NSURLSessionDataTask *task = [session dataTaskWithRequest:request
                                         completionHandler:
     ^(NSData *data, NSURLResponse *response, NSError *error) {
         // ...
     }];

 [task resume];

Upload task 的建立需要使用一個 request,另外加上一個要上傳的 NSData 物件或者是一個本地檔案的路徑對應的 NSURL

 NSURL *URL = [NSURL URLWithString:@"http://example.com/upload"];
 NSURLRequest *request = [NSURLRequest requestWithURL:URL];
 NSData *data = ...;

 NSURLSession *session = [NSURLSession sharedSession];
 NSURLSessionUploadTask *uploadTask = [session uploadTaskWithRequest:request
                                                            fromData:data
                                                   completionHandler:
     ^(NSData *data, NSURLResponse *response, NSError *error) {
         // ...
     }];

 [uploadTask resume];

Download task 也需要一個 request,不同之處在於 completionHandler 這個 block。Data task 和 upload task 會在任務完成時一次性返回,但是 Download task 是將資料一點點地寫入本地的臨時檔案。所以在 completionHandler 這個 block 裡,我們需要把檔案從一個臨時地址移動到一個永久的地址儲存起來:

 NSURL *URL = [NSURL URLWithString:@"http://example.com/file.zip"];
 NSURLRequest *request = [NSURLRequest requestWithURL:URL];

 NSURLSession *session = [NSURLSession sharedSession];
 NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request
                                                         completionHandler:
    ^(NSURL *location, NSURLResponse *response, NSError *error) {
        NSString *documentsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
        NSURL *documentsDirectoryURL = [NSURL fileURLWithPath:documentsPath];
        NSURL *newFileLocation = [documentsDirectoryURL URLByAppendingPathComponent:[[response URL] lastPathComponent]];
        [[NSFileManager defaultManager] copyItemAtURL:location toURL:newFileLocation error:nil];
    }];

 [downloadTask resume];

編者注 原文中這塊程式碼以及上文的表述中存有一些問題,詳見這個 issue,本文已進行更正,如果您有不同意見,歡迎在 Github 上給我們反饋。

NSURLSession 與 NSURLConnection 的 delegate 方法

總體而言,NSURLSession 的 delegate 方法是 NSURLConnection 的演化的十年中對於 ad-hoc 模式的一個顯著改善。您可以檢視這個對映表來進行一個完整的概覽。

以下是一些具體的觀察:

NSURLSession 既擁有 seesion 的 delegate 方法,又擁有 task 的 delegate 方法用來處理鑑權查詢。session 的 delegate 方法處理連線層的問題,諸如伺服器信任,客戶端證書的評估,NTLM 和 Kerberos 協議這類問題,而 task 的 delegate 則處理以網路請求為基礎的問題,如 Basic,Digest,以及代理身份驗證(Proxy authentication)等。

在 NSURLConnection 中有兩個 delegate 方法可以表明一個網路請求已經結束:NSURLConnectionDataDelegate 中的 -connectionDidFinishLoading: 和 NSURLConnectionDelegate 中的 -connection:didFailWithError:,而在 NSURLSession中改為一個 delegate 方法:NSURLSessionTaskDelegate 的 -URLSession:task:didCompleteWithError:

NSURLSession 中表示傳輸多少位元組的引數型別現在改為 int64_t,以前在 NSURLConnection 中相應的引數的型別是 long long

由於增加了 completionHandler: 這個 block 作為引數,NSURLSession 實際上給 Foundation 框架引入了一種全新的模式。這種模式允許 delegate 方法可以安全地在主執行緒與執行,而不會阻塞主執行緒;Delgate 只需要簡單地呼叫 dispatch_async 就可以切換到後臺進行相關的操作,然後在操作完成時呼叫 completionHandler 即可。同時,它還可以有效地擁有多個返回值,而不需要我們使用笨拙的引數指標。以 NSURLSessionTaskDelegate 的 -URLSession:task:didReceiveChallenge:completionHandler: 方法來舉例,completionHandler 接受兩個引數:NSURLSessionAuthChallengeDisposition 和 NSURLCredential,前者為應對鑑權查詢的策略,後者為需要使用的證書(僅當前者——應對鑑權查詢的策略為使用證書,即 NSURLSessionAuthChallengeUseCredential 時有效,否則該引數為 NULL

NSURLSessionConfiguration

NSURLSessionConfiguration 物件用於對 NSURLSession 物件進行初始化。NSURLSessionConfiguration 對以前 NSMutableURLRequest 所提供的網路請求層的設定選項進行了擴充,提供給我們相當大的靈活性和控制權。從指定可用網路,到 cookie,安全性,快取策略,再到使用自定義協議,啟動事件的設定,以及用於移動裝置優化的幾個新屬性,你會發現使用 NSURLSessionConfiguration 可以找到幾乎任何你想要進行配置的選項。

NSURLSession 在初始化時會把配置它的 NSURLSessionConfiguration 物件進行一次 copy,並儲存到自己的 configuration 屬性中,而且這個屬性是隻讀的。因此之後再修改最初配置 session 的那個 configuration 物件對於 session 是沒有影響的。也就是說,configuration 只在初始化時被讀取一次,之後都是不會變化的。

NSURLSessionConfiguration 的工廠方法

NSURLSessionConfiguration 有三個類工廠方法,這很好地說明了 NSURLSession 設計時所考慮的不同的使用場景。

+defaultSessionConfiguration 返回一個標準的 configuration,這個配置實際上與 NSURLConnection 的網路堆疊(networking stack)是一樣的,具有相同的共享 NSHTTPCookieStorage,共享 NSURLCache 和共享 NSURLCredentialStorage

+ephemeralSessionConfiguration 返回一個預設配置,這個配置中不會對快取,Cookie 和證書進行永續性的儲存。這對於實現像祕密瀏覽這種功能來說是很理想的。

+backgroundSessionConfiguration:(NSString *)identifier 的獨特之處在於,它會建立一個後臺 session。後臺 session 不同於常規的,普通的 session,它甚至可以在應用程式掛起,退出或者崩潰的情況下執行上傳和下載任務。初始化時指定的識別符號,被用於向任何可能在程序外恢復後臺傳輸的守護程序(daemon)提供上下文。

配置屬性

NSURLSessionConfiguration 擁有 20 個配置屬性。熟練掌握這些配置屬性的用處,可以讓應用程式充分地利用其網路環境。

基本配置

HTTPAdditionalHeaders 指定了一組預設的可以設定出站請求(outbound request)的資料頭。這對於跨 session 共享資訊,如內容型別,語言,使用者代理和身份認證,是很有用的。

NSString *userPasswordString = [NSString stringWithFormat:@"%@:%@", user, password];
NSData * userPasswordData = [userPasswordString dataUsingEncoding:NSUTF8StringEncoding];
NSString *base64EncodedCredential = [userPasswordData base64EncodedStringWithOptions:0];
NSString *authString = [NSString stringWithFormat:@"Basic %@", base64EncodedCredential];
NSString *userAgentString = @"AppName/com.example.app (iPhone 5s; iOS 7.0.2; Scale/2.0)";

configuration.HTTPAdditionalHeaders = @{@"Accept": @"application/json",
                                        @"Accept-Language": @"en",
                                        @"Authorization": authString,
                                        @"User-Agent": userAgentString};

networkServiceType 對標準的網路流量,網路電話,語音,視訊,以及由一個後臺程序使用的流量進行了區分。大多數應用程式都不需要設定這個。

allowsCellularAccess 和 discretionary 被用於節省通過蜂窩網路連線的頻寬。對於後臺傳輸的情況,推薦大家使用 discretionary 這個屬性,而不是 allowsCellularAccess,因為前者會把 WiFi 和電源的可用性考慮在內。

timeoutIntervalForRequest 和 timeoutIntervalForResource 分別指定了對於請求和資源的超時間隔。許多開發人員試圖使用 timeoutInterval 去限制傳送請求的總時間,但其實它真正的含義是:分組(packet)之間的時間。實際上我們應該使用 timeoutIntervalForResource 來規定整體超時的總時間,但應該只將其用於後臺傳輸,而不是使用者實際上可能想要去等待的任何東西。

HTTPMaximumConnectionsPerHost 是 Foundation 框架中 URL 載入系統的一個新的配置選項。它曾經被 NSURLConnection 用於管理私有的連線池。現在有了 NSURLSession,開發者可以在需要時限制連線到特定主機的數量。

HTTPShouldUsePipelining 這個屬性在 NSMutableURLRequest 下也有,它可以被用於開啟 ,這可以顯著降低請求的載入時間,但是由於沒有被伺服器廣泛支援,預設是禁用的。

sessionSendsLaunchEvents 是另一個新的屬性,該屬性指定該 session 是否應該從後臺啟動。

connectionProxyDictionary 指定了 session 連線中的代理伺服器。同樣地,大多數面向消費者的應用程式都不需要代理,所以基本上不需要配置這個屬性。

HTTPCookieStorage 儲存了 session 所使用的 cookie。預設情況下會使用 NSHTTPCookieShorage 的 +sharedHTTPCookieStorage 這個單例物件,這與 NSURLConnection 是相同的。

HTTPCookieAcceptPolicy 決定了什麼情況下 session 應該接受從伺服器發出的 cookie。

HTTPShouldSetCookies 指定了請求是否應該使用 session 儲存的 cookie,即 HTTPCookieSorage 屬性的值。

安全策略

URLCredentialStorage 儲存了 session 所使用的證書。預設情況下會使用 NSURLCredentialStorage 的 +sharedCredentialStorage 這個單例物件,這與 NSURLConnection 是相同的。

TLSMaximumSupportedProtocol 和 TLSMinimumSupportedProtocol 確定 session 是否支援 SSL 協議

快取策略

URLCache 是 session 使用的快取。預設情況下會使用 NSURLCache 的 +sharedURLCache 這個單例物件,這與 NSURLConnection是相同的。

requestCachePolicy specifies when a cached response should be returned for a request. This is equivalent to NSURLRequest -cachePolicy.

requestCachePolicy 指定了一個請求的快取響應應該在什麼時候返回。這相當於 NSURLRequest 的 -cachePolicy 方法。

自定義協議

protocolClasses 用來配置特定某個 session 所使用的自定義協議(該協議是 NSURLProtocol 的子類)的陣列。

結論

iOS 7 和 Mac OS X 10.9 Mavericks 中 URL 載入系統的變化,是對 NSURLConnection 進行深思熟慮後的一個自然而然的進化。總體而言,蘋果的 Foundation 框架團隊幹了一件令人欽佩的的工作,他們研究並預測了移動開發者現有的和新興的用例,創造了能夠滿足日常任務而且非常好用的 API 。

儘管在這個體系結構中,某些決定對於可組合性和可擴充套件性而言是一種倒退,但是 NSURLSession 仍然是實現更高級別網路功能的一個強大的基礎框架。