使用NSURLSession(UsingNSURLSession)
NSURLSession和其相關的類提供了通過HTTP下載數據的API.該API提供了豐富的代理方法來支持信息身份認證,以及當app未運行時(比如,在iOS中,app掛起狀態)的後台下載功能.
為了使用NSURLSession,客戶端會創建一系列對話(sessions),每個會話都匹配一組相關的數據傳輸任務.例如,編碼一個web浏覽器,客戶端可能需要為沒一個標簽或者窗口創建一個會話.對每個會話,客戶端增加一系列任務,每個任務代表了指向一個特定URL得請求(或者HTTP重定向後的URLs).
像大部分網絡API一樣,NSURLSession是異步的.如果你使用系統默認提供的代理,那麼當一個傳輸成功完成或發生錯誤時,你必須提供一個處理結果的塊(block).如果你使用自定義的代理對象,當從服務器接收到數據後,任務對象會調用自定義的代理方法(對文件下載來說,是當傳輸完成時調用).
注意:回調block是自定義代理的一個主要備選方案.如果你通過回調block的方式創建了一個任務,那麼代理方法將不會再被調用.
NSURLSession API提供了狀態和進度屬性,我們能獲得當前狀態以及任務進度值,並能將這些信息傳遞給代理.它還支持取消,重啟(重設),掛起任務,還能從掛起,取消,失敗狀態重新開始任務.
理解URL Session的概念(Understanding
URL Session Concepts)
一個會話中任務的行為取決於3個因素:會話的類型(由一個先前創建的configuration對象的類型決定),任務的類型,以及任務創建時app是否處在前端激活狀態.
會話的類型(Types of Sessions)
NSURLSession支持三種會話類型(由一個先前創建的configuration對象的類型決定):
1:默認會話類型(defaultSessionConfiguration):與其他下載URLs的方法基本類似(NSURLConnection).使用永久磁盤緩存的持久化策略,在用戶鑰匙串(keychain)中存儲認證信息.
2:臨時會話類型(ephemeralSessionConfiguration):不在磁盤上永久化存儲任何信息(緩存,認證),所有信息只保存再與會話關聯的內存中,當會話失效時,所有信息都會被清空.
3:後台會話類型(backgroundSessionConfigurationWithIdentifier):與默認會話類型大致相似,不同點是有一個單獨的線程來處理數據傳輸.後台會話類型還存在一些額外的限制,參見下文後台數據傳輸注意事項Background
Transfer Considerations.
任務的類型(Types of Tasks)
一個會話中,NSURLSession支持三種任務類型:加載數據(data
tasks),下載數據(download tasks),和上傳數據(upload tasks).
1:加載任務類型使用NSData對象來發送和接收數據.使用場景是傳輸短數據,與服務器交互性強的請求.數據加載能周期性的返回獲得的數據(總數據的一部分),或者一次性通過一個下載完成的回調處理整個數據.數據加載並沒有將獲得的數據存儲到文件中,所以不支持後台加載.
2:下載任務類型以文件形式接收數據,支持app處於非運行狀態時在後台下載.
3:上傳任務類型通常以文件形式發送數據,支持app處於非運行狀態時在後台上傳.
後台數據傳輸注意事項(Background Transfer Considerations)
NSURLSession支持app掛起狀態時進行後台數據傳輸.後台傳輸只有在使用後台會話配置對象創建的會話中可用.(調用defaultSessionConfiguration()方法獲取配置對象)
對後台會話來說,因為真實的數據傳輸是在一條單獨的線程中執行的,並且重新開啟一條線程開銷相對將大,所以一部分特性將不被支持,導致存在下面一些限制:
會話必須給每一個委托提供一個代理.(對上傳和下載類型的會話來說,代理方法與進程內傳輸一樣)
1:只支持HTTP和HTTPS協議(不支持自定義協議)
2:重定向的情況將自動轉向重定向後URL.
3:只支持以文件形式上傳
4:如果後台數據傳輸是在app處於後台期間創建的,configuration對象的discretionary屬性將被視為true(表明當程序在後台運作時由系統自己選擇最佳的網絡連接配置,該屬性可以節省通過蜂窩連接的帶寬).
注意:iOS8
and OS X
10.10之前不支持後台加載數據
app的重啟時的表現在iOS和OS X下有些許區別如下:
在iOS中,當一個後台傳輸完成或者需要進行信息認證時,如果此時app沒有運行,iOS將自動在後台重啟app並且調用UIApplicationDelegate對象的application:handleEventsForBackgroundURLSession:completionHandler:方法.這個調用提供一個會話標識並導致app重啟.app應該存儲完成塊(completion
handler),用同樣的會話標識創建一個後台配置(configuration)對象,並通過這個配置對象創建一個會話.這個會話將在後台持續運行.之後,當會話完成了最後一個下載任務,會發送給會話的代理對象一個URLSessionDidFinishEventsForBackgroundURLSession:消息.會話代理對象應當調用並且存儲完成塊.
在iOS和OS X中,當用戶重新運行app時,app應當立即創建一個後台配置對象,使用上一次運行有未完成任務的會話標識(可能包含多個).然後為這些配置對象創建會話.這些新創建的會話同樣會在後台持續運行.
注意:必須為每一個會話正確的指定標識(在創建配置對象時指定),多個會話使用同一個標識將會導致定義不明確.
如果有任務在app掛起期間完成,代理對象的URLSession:downloadTask:didFinishDownloadingToURL:,方法將被調用.類似的,如果任務需要進行信息認證,會話對象會在調用代理的URLSession:task:didReceiveChallenge:completionHandler:方法或者URLSession:didReceiveChallenge:completionHandler:方法.
在後台的上傳和下載任務將自動恢復加載在網絡出錯之後,所以並沒有必要使用網絡監聽相關API確定什麼時候恢復失敗任務。對於更多怎樣在後台傳輸數據,可以看Simple
Background Transfer.
生命周期和代理交互(Life Cycle and Delegate Interaction)
為了更好的使用NSURLSession,最好深入理解會話的生命周期,以及會話是如何與其代理對象進行交互的.比如代理何時被調用,當服務器返回一個重定向URL時的處理,當一個任務下載失敗時的處理等等.完整的會話生命周期描述,參見Life
Cycle of a URL Session.
NSCopying行為(NSCopying Behavior)
會話對象和任務對象遵循NSCopying協議如下:
當app復制一個會話或者任務對象時,返回的是這個對象本身(沒有創建新對象).
當app復制一個配置對象時,返回一個新創建的配置對象能夠獨立修改(創建了新對象).
代理類接口(Sample Delegate Class Interface)
Listing 1-1 Sample delegate class interface
#import
typedef void (^CompletionHandlerType)();
@interface MySessionDelegate : NSObject
@property NSURLSession *backgroundSession;
@property NSURLSession *defaultSession;
@property NSURLSession *ephemeralSession;
#if TARGET_OS_IPHONE
@property NSMutableDictionary *completionHandlerDictionary;
#endif
- (void) addCompletionHandler: (CompletionHandlerType) handler forSession: (NSString *)identifier;
- (void) callCompletionHandlerForSession: (NSString *)identifier;
@end
創建並配置一個會話(CreatingandConfiguring
a Session)
NSURLSession提供了大量的配置選項:
1:支持對緩存,cookies,證書的私有存儲,以及對單例會話的特定協議
2:關聯到一個特定請求(任務),或者一組請求(會話)的認證
3:通過URL上傳或下載文件,支持將元數據分割成基於文件內容的短數據
4:配置每個主機的最大連接數
5:當資源無法在一個確定時間內下載時,配置一個超時時間
6:支持安全傳輸層協議(TLS)的版本區間
7:自定義代理
8:cookie的管理策略
9:HTTP傳輸管理
大部分的配置都在一個configuration對象中設置,可以重要一些基本設置.初始化一個會話對象(session
object)可以進行如下操作:
1:一個configuration對象用來管理會話或任務的行為
2:可選的,一個代理對象用來表示接收數據的進度,會話任務或會話其他事件的進度,比如服務器認證,決定一個加載請求是否可轉換為下載請求,等等
3:如果沒有指定一個代理,NSURLSession對象將使用系統默認提供的代理.在這種方式中,你可以輕松的使用NSURLSession方法替代已存在的sendAsynchronousRequest:queue:completionHandler:方法
注意:如果app需要在後台進行數據傳輸,必須使用自定義代理.
在創建一個會話對象之後,不能再去修改它的configuration對象和代理,除了重新創建一個會話.
清單1-2展示了創建默認會話,臨時會話和後台會話的示例代碼
#if TARGET_OS_IPHONE
self.completionHandlerDictionary = [NSMutableDictionary dictionaryWithCapacity:0];
#endif
/* Create some configuration objects. */
NSURLSessionConfiguration *backgroundConfigObject = [NSURLSessionConfiguration backgroundSessionConfiguration: @"myBackgroundSessionIdentifier"];
NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSessionConfiguration *ephemeralConfigObject = [NSURLSessionConfiguration ephemeralSessionConfiguration];
/* Configure caching behavior for the default session.Note that iOS requires the cache path to be a path relative to the ~/Library/Caches directory, but OS X expects an absolute path.*/
#if TARGET_OS_IPHONE
NSString *cachePath = @"/MyCacheDirectory";
NSArray *myPathList = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *myPath = [myPathList objectAtIndex:0];
NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
NSString *fullCachePath = [[myPath stringByAppendingPathComponent:bundleIdentifier] stringByAppendingPathComponent:cachePath];
NSLog(@"Cache path: %@\n", fullCachePath);
#else
NSString *cachePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"/nsurlsessiondemo.cache"];
NSLog(@"Cache path: %@\n", cachePath);
#endif
NSURLCache *myCache = [[NSURLCache alloc] initWithMemoryCapacity: 16384 diskCapacity: 268435456 diskPath: cachePath];
defaultConfigObject.URLCache = myCache;
defaultConfigObject.requestCachePolicy = NSURLRequestUseProtocolCachePolicy;
/* Create a session for each configurations. */
self.defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
self.backgroundSession = [NSURLSession sessionWithConfiguration: backgroundConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
self.ephemeralSession = [NSURLSession sessionWithConfiguration: ephemeralConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
除了後台配置對象(background configurations),我們可以重用配置對象來創建其他的會話.(不能重用後台配置對象是因為兩個後台會話不能使用相同的標識符identifier)
你可以在任何時間安全的修改一個configuration對象.因為當創建一個會話時,configuration對象的傳遞是由深拷貝實現的,所以修改只會影響之後新創建的會話,不會對已存在的會話造成影響.例如,你可能想創建另一個只有在WiFi環境下才能重連數據的會話,如1-3中所示:
Listing 1-3 Creating a second session with the same configuration object
ephemeralConfigObject.allowsCellularAccess = NO;
// ...
NSURLSession *ephemeralSessionWiFiOnly = [NSURLSession sessionWithConfiguration: ephemeralConfigObject delegate: self delegateQueue: [NSOperationQueue mainQueue]];
使用系統提供的代理抓取資源(Fetching Resources Using System-Provided Delegates)
最簡單直接的使用NSURLSession的方法是用來替換掉之前的sendAsynchronousRequest:queue:completionHandler:方法.使用該操作,我們需要在app中實現兩處代碼:
1:創建configuration對象,以及一個基於該configuration對象的會話對象
2:一個完成處理程序來處理數據接收完成後要做的事情
使用系統提供的代理,你可以每個請求只用一行代碼來抓取特定URL.清單1-4示例了最簡單的實現.
注意:系統提供的代理僅僅實現了限制了定制網絡行為.如果app的需求超出了基本的URL加載,比如自定義認證或者數據後台下載,那麼就需要實現一個完整的代理,參見URL
Session的生命周期.
Listing 1-4 Requesting a resource using system-provided delegates
NSURLSession *delegateFreeSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate: nil delegateQueue: [NSOperationQueue mainQueue]];
[[delegateFreeSession dataTaskWithURL: [NSURL URLWithString: @"http://www.example.com/"]
completionHandler:^(NSData *data, NSURLResponse *response,
NSError *error) {
NSLog(@"Got response %@ with error %@.\n", response, error);
NSLog(@"DATA:\n%@\nEND DATA\n",
[[NSString alloc] initWithData: data
encoding: NSUTF8StringEncoding]);
}] resume];
使用自定義代理獲取數據(Fetching Data Using a Custom Delegate)
如果使用自定義代理獲取數據,那麼代理必須實現下面兩個方法:
1:URLSession:dataTask:didReceiveData:提供了任務請求返回的數據,周期性的返回數據塊
2:URLSession:task:didCompleteWithError:表明數據是否全部完成接收
如果app需要在URLSession:dataTask:didReceiveData:方法返回之後使用數據,必須用代碼實現數據存儲.
例如,一個web浏覽器可能需要根據之前接收的數據來渲染當前接收的數據.要實現這個功能可以使用一個NSMutableData對象來存儲結果數據,然後使用appendData:
來將當前接收的數據拼接到之前接收到的數據中.
Listing 1-5 shows how you create and start a data task. Data task example
NSURL *url = [NSURL URLWithString: @"http://www.example.com/"];
NSURLSessionDataTask *dataTask = [self.defaultSession dataTaskWithURL: url];
[dataTask resume];
下載文件(Downloading Files)
某種程序上,下載文件和接收數據類似.app應當實現以下的代理方法:
1:URLSession:downloadTask:didFinishDownloadingToURL:提供app下載內容的臨時存儲目錄.
注意:在這個方法返回之前,必須打開文件來進行讀取或者將下載內容移動到一個永久目錄.當方法返回後,臨時文件將會被刪除.
2:
URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:
提供了下載進度的狀態信息.
3:URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:告訴app嘗試恢復之前失敗的下載.
4:URLSession:task:didCompleteWithError::告訴app下載失敗.
如果將下載任務安排在後台會話中,在app非運行期間下載行為仍將繼續.如果將下載任務安排在系統默認會話或者臨時會話中,當app重新啟動時,下載也將重新開始.
在跟服務器傳輸數據期間,如果用戶進行了暫停操作,app可以調用cancelByProducingResumeData:方法取消任務.然後,app可以將已傳輸的數據作為參數傳遞給downloadTaskWithResumeData:或者downloadTaskWithResumeData:completionHandler:來創建一個新的下載任務繼續下載.
清單1-6示例了一個大文件的下載.清單1-7示例了下載任務的代理方法.
Listing 1-6 Download task example
NSURL *url = [NSURL URLWithString: @"https://developer.apple.com/library/ios/documentation/Cocoa/Reference/"
"Foundation/ObjC_classic/FoundationObjC.pdf"];
NSURLSessionDownloadTask *downloadTask = [self.backgroundSession downloadTaskWithURL: url];
[downloadTask resume];
Listing 1-7 Delegate methods for download tasks
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
NSLog(@"Session %@ download task %@ finished downloading to URL %@\n",
session, downloadTask, location);
#if 0
/* Workaround */
[self callCompletionHandlerForSession:session.configuration.identifier];
#endif
#define READ_THE_FILE 0
#if READ_THE_FILE
/* Open the newly downloaded file for reading. */
NSError *err = nil;
NSFileHandle *fh = [NSFileHandle fileHandleForReadingFromURL:location
error: &err];
/* Store this file handle somewhere, and read data from it. */
#else
NSError *err = nil;
NSFileManager *fileManager = [NSFileManager defaultManager];
NSString *cacheDir = [[NSHomeDirectory()
stringByAppendingPathComponent:@"Library"]
stringByAppendingPathComponent:@"Caches"];
NSURL *cacheDirURL = [NSURL fileURLWithPath:cacheDir];
if ([fileManager moveItemAtURL:location
toURL:cacheDirURL
error: &err]) {
/* Store some reference to the new URL */
} else {
/* Handle the error. */
}
#endif
}
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
NSLog(@"Session %@ download task %@ wrote an additional %lld bytes (total %lld bytes) out of an expected %lld bytes.\n",
session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite);
}
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes
{
NSLog(@"Session %@ download task %@ resumed at offset %lld bytes out of an expected %lld bytes.\n",
session, downloadTask, fileOffset, expectedTotalBytes);
}
上傳數據內容(Uploading Body Content)
app能通過三種方式通過提供HTTP POST請求體內容:NSData對象,文件和流.通常情況下,appy應該:
1:使用一個NSData對象,如果內存中已經存在相應數據,並且數據不會被無理由的銷毀.
2:使用文件形式,如果要上傳的內容是通過文件形式存儲在硬盤中的,如果是後台傳輸,或者APP有利於將要上傳的內容寫入文件,這樣可以釋放內存相關的數據。
3:使用流,如果是從網絡接收數據,或者轉化已存在的提供了流的NSURLConnection代碼.
無論你選擇了哪種方法,如果app提供了自定義代理,都應該實現
URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:
方法來獲取上傳進度信息.
此外,如果app使用流作為請求體,還必須提供一個自定義會話代理實現URLSession:task:needNewBodyStream:方法,詳細描述在通過流上傳數據Uploading
Body Content Using a Stream.
使用NSData對象上傳(Uploading
Body Content Using an NSData Object)
使用NSData對象上傳數據,app需要調用uploadTaskWithRequest:fromData:或者uploadTaskWithRequest:fromData:completionHandler:來創建一個上傳任務,將要上傳的NSData對象傳遞給fromData參數.
會話對象根據NSData對象計算內容長度,賦值給請求頭的Content-Length.app必須在URL
request對象中提供服務器可能需要的請求頭信息-例如:content type.
使用文件形式上傳(ploading Body Content Using a File)
使用文件形式上傳,app需要調用
uploadTaskWithRequest:fromFile:
或者uploadTaskWithRequest:fromFile:completionHandler:方法來創建一個上傳任務,以及提供一個文件路徑來讀取內容.
會話對象自動計算Content-Length,如果app沒有提供Content-Type,會話對象將自動生成一個.app還要在URL
request對象中提供服務器可能需要的請求頭信息.
使用流形式上傳(Uploading Body Content Using a Stream)
使用流來上傳信息,app需要調用uploadTaskWithStreamedRequest:方法來創建一個上傳任務.app提供一個與stream相關的request對象讀取請求體內容.app必須在URL
request對象中提供服務器可能需要的請求頭信息,比如content-type和content-length.
此外,因為會話對象不能保證必定能從提供的流中讀取數據,所以app需要提供一個新的流以便會話重新進行請求(比如,認證失敗).app需要實現URLSession:task:needNewBodyStream:方法.當這個方法被調用時,app需要取得或者創建一個新的流,然後調用提供的完成處理塊.
注意:因為app必須實現URLSession:task:needNewBodyStream:方法,所以這種形式不支持使用系統默認的代理.
使用下載任務來上傳文件(Uploading a File Using a Download Task)
當下載任務創建時,app需要提供一個NSData對象或者一個流作為NSURLRequest對象的參數.
如果使用數據流,app需要實現URLSession:task:needNewBodyStream:方法來處理認證失敗的情況.詳細描述在通過流上傳數據Uploading
Body Content Using a Stream.
處理認證和安全傳輸確認(Handling AuthenticationandCustom
TLS Chain Validation)
如果遠程服務器返回一個狀態值表明需要進行認證或者認證需要特定的環境(例如一個SSL客戶端證書),NSURLSession調用會調用一個認證相關的代理方法.
1:會話級別:
NSURLAuthenticationMethodNTLM
,
NSURLAuthenticationMethodNegotiate
,
NSURLAuthenticationMethodClientCertificate
,
or
NSURLAuthenticationMethodServerTrust
,會話對象調用會話代理方法
URLSession:didReceiveChallenge:completionHandler:
.如果app沒有提供會話代理,會話對象調用任務得代理方法
URLSession:task:didReceiveChallenge:completionHandler:
處理.
2:非會話級別:NSURLSession對象調用會話代理方法URLSession:task:didReceiveChallenge:completionHandler:.如果app提供了會話代理,而且app需要處理認證,那麼你必須在任務級別進行處理.在非會話級別上,
URLSession:didReceiveChallenge:completionHandler:
不會被調用.
當以流的形式上傳,認證失敗,任務將不再在重要該流進行上傳。相反,NSURLSession對象將告訴代理URLSession:task:needNewBodyStream:方法獲取應該信得NSInputStream
對象提供數據進行新的請求。對於更多信息關於NSURLSession的授權代理方法,可以看Authentication
Challenges and TLS Chain Validation.
處理iOS後台活動(Handling
iOS Background Activity)
在iOS中使用NSURLSession,當一個下載任務完成時,app將會自動重啟.app代理方法application:handleEventsForBackgroundURLSession:completionHandler:負責重建合適的會話,存儲完成處理塊,並在會話對象調用會話代理的URLSessionDidFinishEventsForBackgroundURLSession:方法時調用完成處理塊.
清單1-8示例了這些會話代理方法
Listing 1-8 Session delegate methods for iOS background downloads
#if TARGET_OS_IPHONE
-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
NSLog(@"Background URL session %@ finished events.\n", session);
if (session.configuration.identifier)
[self callCompletionHandlerForSession: session.configuration.identifier];
}
- (void) addCompletionHandler: (CompletionHandlerType) handler forSession: (NSString *)identifier
{
if ([ self.completionHandlerDictionary objectForKey: identifier]) {
NSLog(@"Error: Got multiple handlers for a single session identifier. This should not happen.\n");
}
[ self.completionHandlerDictionary setObject:handler forKey: identifier];
}
- (void) callCompletionHandlerForSession: (NSString *)identifier
{
CompletionHandlerType handler = [self.completionHandlerDictionary objectForKey: identifier];
if (handler) {
[self.completionHandlerDictionary removeObjectForKey: identifier];
NSLog(@"Calling completion handler.\n");
handler();
}
}
#endif
清單1-9示例了APP代理方法:
Listing 1-9 App delegate methods for iOS background downloads
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler
{
NSURLSessionConfiguration *backgroundConfigObject = [NSURLSessionConfiguration backgroundSessionConfiguration: identifier];
NSURLSession *backgroundSession = [NSURLSession sessionWithConfiguration: backgroundConfigObject delegate: self.mySessionDelegate delegateQueue: [NSOperationQueue mainQueue]];
NSLog(@"Rejoining session %@\n", identifier);
[ self.mySessionDelegate addCompletionHandler: completionHandler forSession: identifier];
}