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
NSURLResponse
、NSURLProtocol
、 NSURLCache
、 NSHTTPCookieStorage
、NSURLCredentialStorage
以及同名類 NSURLConnection
。
NSURLRequest
被傳遞給 NSURLConnection
。被委託物件(遵守以前的非正式協議 <NSURLConnectionDelegate>
和 <NSURLConnectionDataDelegate>
)非同步地返回一個 NSURLResponse
以及包含伺服器返回資訊的 NSData
。
在一個請求被髮送到伺服器之前,系統會先查詢共享的快取資訊,然後根據策略(policy)
在把請求傳送給伺服器的過程中,伺服器可能會發出鑑權查詢(authentication challenge),這可以由共享的 cookie 或機密儲存(credential storage)來自動響應,或者由被委託物件來響應。傳送中的請求也可以被註冊的 NSURLProtocol
物件所攔截,以便在必要的時候無縫地改變其載入行為。
不管怎樣,NSURLConnection
作為網路基礎架構,已經服務了成千上萬的 iOS 和 Mac OS 程式,並且做的還算相當不錯。但是這些年,一些用例——尤其是在 iPhone 和 iPad 上面——已經對 NSURLConnection
在 2013 的 WWDC 上,蘋果推出了 NSURLConnection
的繼任者:NSURLSession
。
和 NSURLConnection
一樣,NSURLSession
指的也不僅是同名類 NSURLSession
,還包括一系列相互關聯的類。NSURLSession
包括了與之前相同的元件,NSURLRequest
與 NSURLCache
,但是把 NSURLConnection
替換成了 NSURLSession
、NSURLSessionConfiguration
以及 NSURLSessionTask
的
3 個子類:NSURLSessionDataTask
,NSURLSessionUploadTask
,NSURLSessionDownloadTask
。
與 NSURLConnection
相比,NSURLsession
最直接的改進就是可以配置每個
session 的快取,協議,cookie,以及證書策略(credential policy),甚至跨程式共享這些資訊。這將允許程式和網路基礎框架之間相互獨立,不會發生干擾。每個 NSURLSession
物件都由一個 NSURLSessionConfiguration
物件來進行初始化,後者指定了剛才提到的那些策略以及一些用來增強移動裝置上效能的新選項。
NSURLSession
中另一大塊就是 session task。它負責處理資料的載入以及檔案和資料在客戶端與服務端之間的上傳和下載。NSURLSessionTask
與 NSURLConnection
最大的相似之處在於它也負責資料的載入,最大的不同之處在於所有的
task 共享其創造者 NSURLSession
這一公共委託者(common delegate)。
我們先來深入探討 task,過後再來討論 NSURLSessionConfiguration
。
NSURLSessionTask
NSURLsessionTask
是一個抽象類,其下有 3 個實體子類可以直接使用:NSURLSessionDataTask
、NSURLSessionUploadTask
、NSURLSessionDownloadTask
。這
3 個子類封裝了現代程式三個最基本的網路任務:獲取資料,比如 JSON 或者 XML,上傳檔案和下載檔案。
當一個 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 連線中的代理伺服器。同樣地,大多數面向消費者的應用程式都不需要代理,所以基本上不需要配置這個屬性。
Cookie 策略
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
仍然是實現更高級別網路功能的一個強大的基礎框架。