iOS 7 和 Mac OS X 10.9 Mavericks 中一個顯著的變化就是對 Foundation URL 加載系統(tǒng)的徹底重構(gòu)。
現(xiàn)在已經(jīng)有人在深入蘋果的網(wǎng)絡(luò)層基礎(chǔ)架構(gòu)的地方做研究了,所以我想是時候來分享一些對于我對于這些新的 API 的看法和心得了,新的 API 將如何影響我們編寫程序,以及它們對于 API 設(shè)計理念的影響。
NSURLConnection
作為 Core Foundation / CFNetwork 框架的 API 之上的一個抽象,在 2003 年,隨著第一版的 Safari 的發(fā)布就發(fā)布了。NSURLConnection
這個名字,實際上是指代的 Foundation 框架的 URL 加載系統(tǒng)中一系列有關(guān)聯(lián)的組件:NSURLRequest
、NSURLResponse
、NSURLProtocol
、 NSURLCache
、 NSHTTPCookieStorage
、NSURLCredentialStorage
以及同名類 NSURLConnection
。
NSURLRequest
被傳遞給 NSURLConnection
。被委托對象(遵守以前的非正式協(xié)議 <NSURLConnectionDelegate>
和 <NSURLConnectionDataDelegate>
)異步地返回一個 NSURLResponse
以及包含服務(wù)器返回信息的 NSData
。
在一個請求被發(fā)送到服務(wù)器之前,系統(tǒng)會先查詢共享的緩存信息,然后根據(jù)策略(policy)以及可用性(availability)的不同,一個已經(jīng)被緩存的響應(yīng)可能會被立即返回。如果沒有緩存的響應(yīng)可用,則這個請求將根據(jù)我們指定的策略來緩存它的響應(yīng)以便將來的請求可以使用。
在把請求發(fā)送給服務(wù)器的過程中,服務(wù)器可能會發(fā)出鑒權(quán)查詢(authentication challenge),這可以由共享的 cookie 或機(jī)密存儲(credential storage)來自動響應(yīng),或者由被委托對象來響應(yīng)。發(fā)送中的請求也可以被注冊的 NSURLProtocol
對象所攔截,以便在必要的時候無縫地改變其加載行為。
不管怎樣,NSURLConnection
作為網(wǎng)絡(luò)基礎(chǔ)架構(gòu),已經(jīng)服務(wù)了成千上萬的 iOS 和 Mac OS 程序,并且做的還算相當(dāng)不錯。但是這些年,一些用例——尤其是在 iPhone 和 iPad 上面——已經(jīng)對 NSURLConnection
的幾個核心概念提出了挑戰(zhàn),讓蘋果有理由對它進(jìn)行重構(gòu)。
在 2013 的 WWDC 上,蘋果推出了 NSURLConnection
的繼任者:NSURLSession
。
和 NSURLConnection
一樣,NSURLSession
指的也不僅是同名類 NSURLSession
,還包括一系列相互關(guān)聯(lián)的類。NSURLSession
包括了與之前相同的組件,NSURLRequest
與 NSURLCache
,但是把 NSURLConnection
替換成了 NSURLSession
、NSURLSessionConfiguration
以及 NSURLSessionTask
的 3 個子類:NSURLSessionDataTask
,NSURLSessionUploadTask
,NSURLSessionDownloadTask
。
與 NSURLConnection
相比,NSURLsession
最直接的改進(jìn)就是可以配置每個 session 的緩存,協(xié)議,cookie,以及證書策略(credential policy),甚至跨程序共享這些信息。這將允許程序和網(wǎng)絡(luò)基礎(chǔ)框架之間相互獨(dú)立,不會發(fā)生干擾。每個 NSURLSession
對象都由一個 NSURLSessionConfiguration
對象來進(jìn)行初始化,后者指定了剛才提到的那些策略以及一些用來增強(qiáng)移動設(shè)備上性能的新選項。
NSURLSession
中另一大塊就是 session task。它負(fù)責(zé)處理數(shù)據(jù)的加載以及文件和數(shù)據(jù)在客戶端與服務(wù)端之間的上傳和下載。NSURLSessionTask
與 NSURLConnection
最大的相似之處在于它也負(fù)責(zé)數(shù)據(jù)的加載,最大的不同之處在于所有的 task 共享其創(chuàng)造者 NSURLSession
這一公共委托者(common delegate)。
我們先來深入探討 task,過后再來討論 NSURLSessionConfiguration
。
NSURLsessionTask
是一個抽象類,其下有 3 個實體子類可以直接使用:NSURLSessionDataTask
、NSURLSessionUploadTask
、NSURLSessionDownloadTask
。這 3 個子類封裝了現(xiàn)代程序三個最基本的網(wǎng)絡(luò)任務(wù):獲取數(shù)據(jù),比如 JSON 或者 XML,上傳文件和下載文件。
http://wiki.jikexueyuan.com/project/objc/images/5-11.png" alt="" />
當(dāng)一個 NSURLSessionDataTask
完成時,它會帶有相關(guān)聯(lián)的數(shù)據(jù),而一個 NSURLSessionDownloadTask
任務(wù)結(jié)束時,它會帶回已下載文件的一個臨時的文件路徑。因為一般來說,服務(wù)端對于一個上傳任務(wù)的響應(yīng)也會有相關(guān)數(shù)據(jù)返回,所以 NSURLSessionUploadTask
繼承自 NSURLSessionDataTask
。
所有的 task 都是可以取消,暫停或者恢復(fù)的。當(dāng)一個 download task 取消時,可以通過選項來創(chuàng)建一個恢復(fù)數(shù)據(jù)(resume data),然后可以傳遞給下一次新創(chuàng)建的 download task,以便繼續(xù)之前的下載。
不同于直接使用 alloc-init
初始化方法,task 是由一個 NSURLSession
創(chuàng)建的。每個 task 的構(gòu)造方法都對應(yīng)有或者沒有 completionHandler
這個 block 的兩個版本,例如:有這樣兩個構(gòu)造方法 –dataTaskWithRequest:
和 –dataTaskWithRequest:completionHandler:
。這與 NSURLConnection
的 -sendAsynchronousRequest:queue:completionHandler:
方法類似,通過指定 completionHandler
這個 block 將創(chuàng)建一個隱式的 delegate,來替代該 task 原來的 delegate——session。對于需要 override 原有 session task 的 delegate 的默認(rèn)行為的情況,我們需要使用這種不帶 completionHandler
的版本。
在 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 的構(gòu)造方法上延續(xù)了這一模式。不同的是,這里不會立即運(yùn)行 task,而是將該 task 對象先返回,允許我們進(jìn)一步的配置,然后可以使用 resume
方法來讓它開始運(yùn)行。
Data task 可以通過 NSURL
或 NSURLRequest
創(chuàng)建(使用前者相當(dāng)于是使用一個對于該 URL 進(jìn)行標(biāo)準(zhǔn) 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 的創(chuàng)建需要使用一個 request,另外加上一個要上傳的 NSData
對象或者是一個本地文件的路徑對應(yīng)的 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 會在任務(wù)完成時一次性返回,但是 Download task 是將數(shù)據(jù)一點(diǎn)點(diǎn)地寫入本地的臨時文件。所以在 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,本文已進(jìn)行更正,如果您有不同意見,歡迎在 Github 上給我們反饋。
總體而言,NSURLSession
的 delegate 方法是 NSURLConnection
的演化的十年中對于 ad-hoc 模式的一個顯著改善。您可以查看這個映射表來進(jìn)行一個完整的概覽。
以下是一些具體的觀察:
NSURLSession
既擁有 seesion 的 delegate 方法,又擁有 task 的 delegate 方法用來處理鑒權(quán)查詢。session 的 delegate 方法處理連接層的問題,諸如服務(wù)器信任,客戶端證書的評估,NTLM 和 Kerberos 協(xié)議這類問題,而 task 的 delegate 則處理以網(wǎng)絡(luò)請求為基礎(chǔ)的問題,如 Basic,Digest,以及代理身份驗證(Proxy authentication)等。
在 NSURLConnection
中有兩個 delegate 方法可以表明一個網(wǎng)絡(luò)請求已經(jīng)結(jié)束:NSURLConnectionDataDelegate
中的 -connectionDidFinishLoading:
和 NSURLConnectionDelegate
中的 -connection:didFailWithError:
,而在 NSURLSession
中改為一個 delegate 方法:NSURLSessionTaskDelegate
的 -URLSession:task:didCompleteWithError:
NSURLSession
中表示傳輸多少字節(jié)的參數(shù)類型現(xiàn)在改為 int64_t
,以前在 NSURLConnection
中相應(yīng)的參數(shù)的類型是 long long
。
由于增加了 completionHandler:
這個 block 作為參數(shù),NSURLSession
實際上給 Foundation 框架引入了一種全新的模式。這種模式允許 delegate 方法可以安全地在主線程與運(yùn)行,而不會阻塞主線程;Delgate 只需要簡單地調(diào)用 dispatch_async
就可以切換到后臺進(jìn)行相關(guān)的操作,然后在操作完成時調(diào)用 completionHandler
即可。同時,它還可以有效地?fù)碛卸鄠€返回值,而不需要我們使用笨拙的參數(shù)指針。以 NSURLSessionTaskDelegate
的 -URLSession:task:didReceiveChallenge:completionHandler:
方法來舉例,completionHandler
接受兩個參數(shù):NSURLSessionAuthChallengeDisposition
和 NSURLCredential
,前者為應(yīng)對鑒權(quán)查詢的策略,后者為需要使用的證書(僅當(dāng)前者——應(yīng)對鑒權(quán)查詢的策略為使用證書,即 NSURLSessionAuthChallengeUseCredential
時有效,否則該參數(shù)為 NULL
)
想要查看更多關(guān)于 session task 的信息,可以查看 WWDC Session 705: "What’s New in Foundation Networking"
NSURLSessionConfiguration
對象用于對 NSURLSession
對象進(jìn)行初始化。NSURLSessionConfiguration
對以前 NSMutableURLRequest
所提供的網(wǎng)絡(luò)請求層的設(shè)置選項進(jìn)行了擴(kuò)充,提供給我們相當(dāng)大的靈活性和控制權(quán)。從指定可用網(wǎng)絡(luò),到 cookie,安全性,緩存策略,再到使用自定義協(xié)議,啟動事件的設(shè)置,以及用于移動設(shè)備優(yōu)化的幾個新屬性,你會發(fā)現(xiàn)使用 NSURLSessionConfiguration
可以找到幾乎任何你想要進(jìn)行配置的選項。
NSURLSession
在初始化時會把配置它的 NSURLSessionConfiguration
對象進(jìn)行一次 copy,并保存到自己的 configuration
屬性中,而且這個屬性是只讀的。因此之后再修改最初配置 session 的那個 configuration 對象對于 session 是沒有影響的。也就是說,configuration 只在初始化時被讀取一次,之后都是不會變化的。
NSURLSessionConfiguration
有三個類工廠方法,這很好地說明了 NSURLSession
設(shè)計時所考慮的不同的使用場景。
+defaultSessionConfiguration
返回一個標(biāo)準(zhǔn)的 configuration,這個配置實際上與 NSURLConnection
的網(wǎng)絡(luò)堆棧(networking stack)是一樣的,具有相同的共享 NSHTTPCookieStorage
,共享 NSURLCache
和共享 NSURLCredentialStorage
。
+ephemeralSessionConfiguration
返回一個預(yù)設(shè)配置,這個配置中不會對緩存,Cookie 和證書進(jìn)行持久性的存儲。這對于實現(xiàn)像秘密瀏覽這種功能來說是很理想的。
+backgroundSessionConfiguration:(NSString *)identifier
的獨(dú)特之處在于,它會創(chuàng)建一個后臺 session。后臺 session 不同于常規(guī)的,普通的 session,它甚至可以在應(yīng)用程序掛起,退出或者崩潰的情況下運(yùn)行上傳和下載任務(wù)。初始化時指定的標(biāo)識符,被用于向任何可能在進(jìn)程外恢復(fù)后臺傳輸?shù)?strong>守護(hù)進(jìn)程(daemon)提供上下文。
想要查看更多關(guān)于后臺 session 的信息,可以查看 WWDC Session 204: "What's New with Multitasking"
NSURLSessionConfiguration
擁有 20 個配置屬性。熟練掌握這些配置屬性的用處,可以讓應(yīng)用程序充分地利用其網(wǎng)絡(luò)環(huán)境。
HTTPAdditionalHeaders
指定了一組默認(rèn)的可以設(shè)置出站請求(outbound request)的數(shù)據(jù)頭。這對于跨 session 共享信息,如內(nèi)容類型,語言,用戶代理和身份認(rèn)證,是很有用的。
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
對標(biāo)準(zhǔn)的網(wǎng)絡(luò)流量,網(wǎng)絡(luò)電話,語音,視頻,以及由一個后臺進(jìn)程使用的流量進(jìn)行了區(qū)分。大多數(shù)應(yīng)用程序都不需要設(shè)置這個。
allowsCellularAccess
和 discretionary
被用于節(jié)省通過蜂窩網(wǎng)絡(luò)連接的帶寬。對于后臺傳輸?shù)那闆r,推薦大家使用 discretionary
這個屬性,而不是 allowsCellularAccess
,因為前者會把 WiFi 和電源的可用性考慮在內(nèi)。
timeoutIntervalForRequest
和 timeoutIntervalForResource
分別指定了對于請求和資源的超時間隔。許多開發(fā)人員試圖使用 timeoutInterval
去限制發(fā)送請求的總時間,但其實它真正的含義是:分組(packet)之間的時間。實際上我們應(yīng)該使用 timeoutIntervalForResource
來規(guī)定整體超時的總時間,但應(yīng)該只將其用于后臺傳輸,而不是用戶實際上可能想要去等待的任何東西。
HTTPMaximumConnectionsPerHost
是 Foundation 框架中 URL 加載系統(tǒng)的一個新的配置選項。它曾經(jīng)被 NSURLConnection
用于管理私有的連接池?,F(xiàn)在有了 NSURLSession
,開發(fā)者可以在需要時限制連接到特定主機(jī)的數(shù)量。
HTTPShouldUsePipelining
這個屬性在 NSMutableURLRequest
下也有,它可以被用于開啟 HTTP 管線化(HTTP pipelining),這可以顯著降低請求的加載時間,但是由于沒有被服務(wù)器廣泛支持,默認(rèn)是禁用的。
sessionSendsLaunchEvents
是另一個新的屬性,該屬性指定該 session 是否應(yīng)該從后臺啟動。
connectionProxyDictionary
指定了 session 連接中的代理服務(wù)器。同樣地,大多數(shù)面向消費(fèi)者的應(yīng)用程序都不需要代理,所以基本上不需要配置這個屬性。
關(guān)于連接代理的更多信息可以在
CFProxySupport
Reference 找到。
HTTPCookieStorage
存儲了 session 所使用的 cookie。默認(rèn)情況下會使用 NSHTTPCookieShorage
的 +sharedHTTPCookieStorage
這個單例對象,這與 NSURLConnection
是相同的。
HTTPCookieAcceptPolicy
決定了什么情況下 session 應(yīng)該接受從服務(wù)器發(fā)出的 cookie。
HTTPShouldSetCookies
指定了請求是否應(yīng)該使用 session 存儲的 cookie,即 HTTPCookieSorage
屬性的值。
URLCredentialStorage
存儲了 session 所使用的證書。默認(rèn)情況下會使用 NSURLCredentialStorage
的 +sharedCredentialStorage
這個單例對象,這與 NSURLConnection
是相同的。
TLSMaximumSupportedProtocol
和 TLSMinimumSupportedProtocol
確定 session 是否支持 SSL 協(xié)議。
URLCache
是 session 使用的緩存。默認(rèn)情況下會使用 NSURLCache
的 +sharedURLCache
這個單例對象,這與 NSURLConnection
是相同的。
requestCachePolicy
specifies when a cached response should be returned for a request. This is equivalent to NSURLRequest -cachePolicy
.
requestCachePolicy
指定了一個請求的緩存響應(yīng)應(yīng)該在什么時候返回。這相當(dāng)于 NSURLRequest
的 -cachePolicy
方法。
protocolClasses
用來配置特定某個 session 所使用的自定義協(xié)議(該協(xié)議是 NSURLProtocol
的子類)的數(shù)組。
iOS 7 和 Mac OS X 10.9 Mavericks 中 URL 加載系統(tǒng)的變化,是對 NSURLConnection
進(jìn)行深思熟慮后的一個自然而然的進(jìn)化??傮w而言,蘋果的 Foundation 框架團(tuán)隊干了一件令人欽佩的的工作,他們研究并預(yù)測了移動開發(fā)者現(xiàn)有的和新興的用例,創(chuàng)造了能夠滿足日常任務(wù)而且非常好用的 API 。
盡管在這個體系結(jié)構(gòu)中,某些決定對于可組合性和可擴(kuò)展性而言是一種倒退,但是 NSURLSession
仍然是實現(xiàn)更高級別網(wǎng)絡(luò)功能的一個強(qiáng)大的基礎(chǔ)框架。