`2013`年的`WWDC`大會上,蘋果推出了`NSURLSession`,對`Foundation URL`載入系統進行了徹底的重構,提供了更豐富的`API`來處理網路請求,如:支持`http2.0`協議、直接把數據下載到磁碟、同一`session`發送多個請求、下載是多線程非同步處理和提供全局的`... ...
2013
年的WWDC
大會上,蘋果推出了NSURLSession
,對Foundation URL
載入系統進行了徹底的重構,提供了更豐富的API
來處理網路請求,如:支持http2.0
協議、直接把數據下載到磁碟、同一session
發送多個請求、下載是多線程非同步處理和提供全局的session
並可以統一配置等等,提高了NSURLSession
的易用性、靈活性,更加地適合移動開發的需求。
NSURLSession的介紹
1. session類型
Default session
+defaultSessionConfiguration
返回一個標準的 configuration
,這個配置實際上與 NSURLConnection
的網路堆棧(networking stack
)是一樣的,具有相同的共用 NSHTTPCookieStorage
,共用 NSURLCache
和共用 NSURLCredentialStorage
。
Ephemeral session
+ephemeralSessionConfiguration
返回一個預設配置,這個配置中不會對緩存Cookie
和證書進行持久性的存儲,這對於實現像秘密瀏覽這種功能來說是很理想的。
Background session
+backgroundSessionConfiguration:(NSString *)identifier
的獨特之處在於,它會創建一個後臺 session
。後臺 session
不同於常規的,普通的 session
,它甚至可以在應用程式掛起,退出或者崩潰的情況下進行上傳和下載任務。初始化時指定的標識符,被用於向任何可能在進程外恢復後臺傳輸的守護進程。
2. 配置屬性
基本配置
HTTPAdditionalHeaders
指定了一組預設的可以設置請求(outbound request
)的數據頭。這對於跨 session
共用信息,如內容類型、語言、用戶代理和身份認證,是很有用的。
// 設置請求的header
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
下也有,它可以被用於開啟HTTP
管線化(HTTP pipelining
),這可以顯著降低請求的載入時間,但是由於沒有被伺服器廣泛支持,預設是禁用的。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
指定了一個請求的緩存響應應該在什麼時候返回。這相當於NSURLRequest
的-cachePolicy
方法。
自定義協議
protocolClasses
用來配置特定某個 session
所使用的自定義協議(該協議是 NSURLProtocol
的子類)的數組。
3. NSURLSessionTask
NSURLsessionTask
是一個抽象類,其下有 3
個實體子類可以直接使用:NSURLSessionDataTask
、NSURLSessionUploadTask
、NSURLSessionDownloadTask
。這 3
個子類封裝了現代程式三個最基本的網路任務:獲取數據,比如 JSON
或者 XML
,上傳文件和下載文件。
不同於直接使用 alloc-init
初始化方法,task
是由一個 NSURLSession
創建的。每個 task
的構造方法都對應有或者沒有 completionHandler
這個 block
的兩個版本。
4. 代理
針對NSURLsessionTask
的代理,根代理為NSURLSessionDelegate
,其它的代理直接或者間接繼承自改代理,如:NSURLSessionTaskDelegate
、NSURLSessionDataDelegate
、NSURLSessionDownloadDelegate
。其中根代理NSURLSessionDelegate
主要處理鑒權、後臺下載任務完成通知等等,NSURLSessionTaskDelegate
主要處理收到鑒權響應、任務結束(無論是正常還是異常),NSURLSessionDataDelegate
處理數據的接收、dataTask
轉downloadTask
、緩存等,NSURLSessionDownloadDelegate
主要處理數據下載、數據進度通知等。
NSURLSession應用
1. NSURLSessionDataTask 發送 GET 請求
//確定請求路徑
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/login?username=520&pwd=520&type=JSON"];
//創建 NSURLSession 對象
NSURLSession *session = [NSURLSession sharedSession];
/**
根據對象創建 Task 請求,預設在子線程中解析數據
url 方法內部會自動將 URL 包裝成一個請求對象(預設是 GET 請求)
completionHandler 完成之後的回調(成功或失敗)
param data 返回的數據(響應體)
param response 響應頭
param error 錯誤信息
*/
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url completionHandler:
^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//解析伺服器返回的數據
NSLog(@"%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
}];
//發送請求(執行Task)
[dataTask resume];
2. NSURLSessionDataTask 發送 POST 請求
//確定請求路徑
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/login"];
//創建可變請求對象
NSMutableURLRequest *requestM = [NSMutableURLRequest requestWithURL:url];
//修改請求方法
requestM.HTTPMethod = @"POST";
//設置請求體
requestM.HTTPBody = [@"username=520&pwd=520&type=JSON" dataUsingEncoding:NSUTF8StringEncoding];
//創建會話對象
NSURLSession *session = [NSURLSession sharedSession];
//創建請求 Task
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:requestM completionHandler:
^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//解析返回的數據
NSLog(@"%@", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
}];
//發送請求
[dataTask resume];
3. NSURLSessionDataTask 設置代理髮送請求
//確定請求路徑
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/login"];
//創建可變請求對象
NSMutableURLRequest *requestM = [NSMutableURLRequest requestWithURL:url];
//設置請求方法
requestM.HTTPMethod = @"POST";
//設置請求體
requestM.HTTPBody = [@"username=520&pwd=520&type=JSON" dataUsingEncoding:NSUTF8StringEncoding];
//創建會話對象,設置代理
/**
第一個參數:配置信息
第二個參數:設置代理
第三個參數:隊列,如果該參數傳遞nil 那麼預設在子線程中執行
*/
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
delegate:self delegateQueue:nil];
//創建請求 Task
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:requestM];
//發送請求
[dataTask resume];
代理方法:
-(void)URLSession:(NSURLSession *)session dataTask:(nonnull NSURLSessionDataTask *)dataTask
didReceiveResponse:(nonnull NSURLResponse *)response
completionHandler:(nonnull void (^)(NSURLSessionResponseDisposition))completionHandler {
//子線程中執行
NSLog(@"接收到伺服器響應的時候調用 -- %@", [NSThread currentThread]);
self.dataM = [NSMutableData data];
//預設情況下不接收數據
//必須告訴系統是否接收伺服器返回的數據
completionHandler(NSURLSessionResponseAllow);
}
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
NSLog(@"接受到伺服器返回數據的時候調用,可能被調用多次");
//拼接伺服器返回的數據
[self.dataM appendData:data];
}
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
NSLog(@"請求完成或者是失敗的時候調用");
//解析伺服器返回數據
NSLog(@"%@", [[NSString alloc] initWithData:self.dataM encoding:NSUTF8StringEncoding]);
}
設置代理之後的強引用問題
NSURLSession
對象在使用的時候,如果設置了代理,那麼session
會對代理對象保持一個強引用,在合適的時候應該主動進行釋放- 可以在控制器調用
viewDidDisappear
方法的時候來進行處理,通過調用invalidateAndCancel
方法或者是finishTasksAndInvalidate
方法來釋放對代理對象的強引用。
其中,invalidateAndCancel
是直接取消請求然後釋放代理對象,而finishTasksAndInvalidate
是等請求完成之後釋放代理對象。
4. NSURLSessionDownloadTask 簡單下載
//確定請求路徑
NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/images/minion_02.png"];
//創建請求對象
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//創建會話對象
NSURLSession *session = [NSURLSession sharedSession];
//創建會話請求
//優點:該方法內部已經完成了邊接收數據邊寫沙盒的操作,解決了記憶體飆升的問題
NSURLSessionDownloadTask *downTask = [session downloadTaskWithRequest:request completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//預設存儲到臨時文件夾 tmp 中,需要剪切文件到 cache
NSLog(@"%@", location);//目標位置
NSString *fullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]
stringByAppendingPathComponent:response.suggestedFilename];
/**
fileURLWithPath:有協議頭
URLWithString:無協議頭
*/
[[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:fullPath] error:nil];
}];
//發送請求
[downTask resume];
以上方法無法監聽下載進度,如要獲取下載進度,可以使用代理的方式進行下載。
5. NSURLSessionDownloadTask 代理方式
NSURL * url = [NSURL URLWithString:@"http://e.hiphotos.baidu.com/image/pic/item/63d0f703918fa0ec14b94082249759ee3c6ddbc6.jpg"];
NSURLSessionConfiguration *defaultConfigObject = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *defaultSession = [NSURLSession sessionWithConfiguration: defaultConfigObject delegate:self delegateQueue: [NSOperationQueue mainQueue]];
NSURLSessionDownloadTask * downloadTask =[ defaultSession downloadTaskWithURL:url];
[downloadTask resume];
代理方法:
// 接收數據,可能多次被調用
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
float progress = totalBytesWritten * 1.0/totalBytesExpectedToWrite;
// 主線程更新UI
dispatch_async(dispatch_get_main_queue(),^ {
[self.process setProgress:progress animated:YES];
});
}
// 3.下載完成之後調用該方法
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
NSString *catchDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString *filePath = [catchDir stringByAppendingPathComponent:@"app.dmg"];
NSError *fileError = nil;
NSURL *fileURL = [NSURL fileURLWithPath:filePath];
[[NSFileManager defaultManager] moveItemAtURL:location toURL:fileURL error:&fileError];
if (fileError) {
NSLog(@"保存下載文件出錯:%@", fileError);
} else {
NSLog(@"保存成功:%@", filePath);
}
}
暫停和恢復下載:
方式一:
// 暫停下載
- (IBAction)suspendDownload {
if (self.session) {
__weak typeof(self) weakSelf = self;
[self.task cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
weakSelf.receivedData = resumeData;
}];
}
}
// 恢復下載
- (IBAction)resumeDownload {
if (self.session) {
self.task = [self.session downloadTaskWithResumeData:self.receivedData];
}
[self.task resume];
}
方式二:
//暫停
[self.downloadTask suspend];
//恢復
[self.downloadTask resume];
6. NSURLSessionDownloadTask 後臺下載
// 後臺session
- (NSURLSession* ) backgroundURLSession {
static NSURLSession * session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString * identifier = @"com.yourcompany.appId.BackgroundSession";
NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:identifier];
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:[NSOperationQueue mainQueue]];
});
return session;
}
// 創建並啟動任務
- (void)beginDownloadWithUrl:(NSString *)downloadURLString {
NSURL *downloadURL = [NSURL URLWithString:downloadURLString];
NSURLRequest *request = [NSURLRequest requestWithURL:downloadURL];
NSURLSession *session = [self backgroundURLSession];
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request];
[downloadTask resume];
}
在appDelegate
中實現application:handleEventsForBackgroundURLSession:completionHandler:
方法,在後臺所有的任務完成後會調用給方法,但是我一直沒有調用成功,原因未知,高手可以告知一下
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
NSURLSession *backgroundSession = [self backgroundURLSession];
NSLog(@"Rejoining session with identifier %@ %@", identifier, backgroundSession);
// 保存 completion handler 以在處理 session 事件後更新 UI
[self addCompletionHandler:completionHandler forSession:identifier];
}
handleEventsForBackgroundURLSession
方法是在後臺下載的所有任務完成後才會調用。如果後臺任務完成且應用被殺掉,啟動應用程式後,該方法會在 application:didFinishLaunchingWithOptions:
方法被調用之後被調用。
//NSURLSessionDelegate委托方法,會在NSURLSessionDownloadDelegate委托方法後執行
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
NSLog(@"Background URL session %@ finished events.\n", session);
}
之後會調用接收完成的方法:
/*
* 在該方法結束前,需要處理location指向的文件,因為方法結束後,臨時文件會被銷毀
* 如果用模擬器保存,會出錯,因為模擬器上app退出後再啟動是,路徑會不一樣,導致找不到後臺下載的文件;而用真機調試則無此問題 !!!
*/
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{}
/*
* 該方法下載成功和失敗都會回調,只是失敗的是error是有值的,
* 在下載失敗時,error的userinfo屬性可以通過NSURLSessionDownloadTaskResumeData
* 這個key來取到resumeData(和上面的resumeData是一樣的),再通過resumeData恢復下載
*/
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{}
7. NSURLSessionUploadTask上傳任務
NSURL*URL = [NSURLURLWithString:@"http://example.com/upload"];
NSURLRequest*request = [NSURLRequestrequestWithURL:URL];
NSData*data = ...;
NSURLSession*session = [NSURLSessionsharedSession];
NSURLSessionUploadTask*uploadTask = [session uploadTaskWithRequest:request fromData:datacompletionHandler:^(NSData*data, NSURLResponse *response,NSError*error) {
// ...
}];
[uploadTask resume];
註意事項
1. 後臺下載的配置和限制
作為一個必須實現的委托,您不能對NSURLSession
使用簡單的基於 block
的回調方法。後臺啟動應用程式,是相對耗費較多資源的,所以總是採用HTTP
重定向。後臺傳輸服務只支持HTTP
和HTTPS
,你不能使用自定義的協議。系統會根據可用的資源進行優化,在任何時候你都不能強制傳輸任務在後臺進行。
另外,要註意的是在後臺會話中,NSURLSessionDataTasks
是完全不支持的,你應該只出於短期的、小請求等使用這些任務,而不是用來下載或上傳。
2. 後臺啟動新的下載
蘋果會對後臺的下載任務進行限制,大致流程如下:
- 蘋果的
NSURLSession
這個類會維護一個Delay
值(即延時執行時間),用於後臺啟動任務延時執行時使用; - 當在後臺啟動一個新任務時,蘋果會對這個任務進行延時執行,延時時間蘋果那邊是有一個預設的延時時間,當後臺啟動的任務數越多,這個值就會成
2
的N-1
冪倍增長; - 比如:假設蘋果設定的延時時間為
Delay
。當在後臺啟動了第一個任務時,這個任務的延時時間為Delay
,這個任務會在Delay
時間後開始執行;當啟動在後臺啟動第二個任務時,這個任務的延時時間為:2 * Delay
,當啟動第三個任務是,該任務的延時執行時間即為:2 * 2 * Delay
;以此類推,在後臺啟動第N個任務是,該任務的延時執行時間為:2^(N-1)次方 * Delay
; - 但是在應用從後臺切到前臺或者重新啟動時,這個延時時間會重置。
參考示例:
https://github.com/BirdandLion/NSURLSessionDemo
參考資料:
Life Cycle of a URL Session
http://www.jianshu.com/p/63e2ad28459f
http://www.jianshu.com/p/b0ddadd34037
http://www.jianshu.com/p/1211cf99dfc3
http://www.jianshu.com/p/02a5a896c9ed