在2013年WWDC上蘋果揭開了NSURLSession的面紗,將它作爲NSURLConnection的繼任者。現在使用最廣泛的第三方網絡框架:AFNetworking、SDWebImage等等都使用了NSURLSession。作爲iOS開發人員,應該緊隨蘋果的步伐,不斷的學習,無論是軟件的更新、系統的更新、API的更新,而不能墨守成規。
* 相比較NSURLConnection,NSURLSession提供了 配置會話緩存、協議、cookie和證書能力,這使得網絡架構和應用程序可以獨立工作、互不干擾。
* 另外,NSURLSession另一個重要的部分是 會話任務,它負責加載數據,在客戶端和服務器端進行文件的上傳下載。
下面讓我們正式進入NSURLSession學習。
一、NSURLSession介紹
在NSURLSession時代,網絡請求基本上由3個任務完成:
NSURLSessionData
:請求數據任務NSURLSessionUploadTask
:請求上傳任務NSURLSessionDownloadTask
:請求下載任務
關係圖如下:
NSURLSessionTask
支持任務的暫停、取消和恢復,並且默認任務運行在其他非主線程中
二、NSURLSession使用
說了這麼多,是時候來露兩手了,具體NSURLSession怎麼用呢?
1. 數據請求
先看一個網絡數據請求實例,和上一章的NSURLConnection請求對比參考:
- (void)loadJsonData{
//1.創建url
NSString *urlStr = @"http://192.168.1.208/ViewStatus.aspx?userName=KenshinCui&password=123";
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:urlStr];
//2.創建請求
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//3.創建會話(這裏使用了一個全局會話)
NSURLSession *session = [NSURLSession sharedSession];
//4.通過會話創建任務
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error) {
NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@",dataStr);
}else{
NSLog(@"error is :%@",error.localizedDescription);
}
}];
//5.每一個任務默認都是掛起的,需要調用 resume 方法啓動任務
[dataTask resume];
}
不難發現NSURLSession網絡請求的五步走黃金油戰略:
1. 創建NSURL
2. 創建NSURLRequest
3. 創建會話NSURLSession
4. 通過會話創建任務NSURLSessionTask
的子類
5. 調用resume
方法,啓動任務
2. 文件下載
文件下載也是一樣的,只是換上下載任務NSURLSessionDownloadTask
就行,對回調做不同處理,一切都要貫徹五步走戰略,O(∩_∩)O哈!
常用的創建文件下載任務的方法如下:
/* 回調類型,這是我爲了排版方便抽出來的,實際框架中沒有 */
typedef void (^downloadCompletionBlock)(NSURL*,NSURLReponse*,NSError*);
/* 創建文件下載任務,需要請求NSURLRequest */
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request
completionHandler:(downloadCompletionBlock)completion;
/* 創建文件任務,簡化了一些操作,只需要URL就能進行文件下載 */
- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url
completionHandler:(downloadCompletionBlock)completion;
下面是下載實例
-(void)downloadFile{
//1.創建url
NSString *fileName = @"1.jpg";
NSString *urlStr = [NSString stringWithFormat: @"http://192.168.1.208/FileDownload.aspx?file=%@",fileName];
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:urlStr];
//2.創建請求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
//3.創建會話(這裏使用了一個全局會話)
NSURLSession *session = [NSURLSession sharedSession];
//4.創建文件下載任務
NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request
completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
if (!error) {
//注意location是下載後的臨時保存路徑,需要將它移動到需要保存的位置
NSError *saveError;
NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString *savePath = [cachePath stringByAppendingPathComponent:fileName];
NSURL *saveUrl = [NSURL fileURLWithPath:savePath];
[[NSFileManager defaultManager] copyItemAtURL:location toURL:saveUrl error:&saveError];
if (!saveError) {
NSLog(@"save sucess.");
}
}
}];
//5.啓動任務
[downloadTask resume];
}
- 回調中的location是下載後的臨時保存路徑,需要將它移動到需要保存的位置
- NSFileManager的對象方法
//將fromURL路徑下的文件拷貝到toURL路徑下
- (void)copyItemAtURL:(NSURL *)fromUrl
toURL:(NSURL *)toUrl
error:(NSError **)error;
3.文件上傳
使用NSURLConnection的文件上傳時,我們還需要自己構建上傳請求,主要是拼接上傳表單,這是個十分麻煩的過程。
現在使用NSURLSessionUploadTask
文件上傳任務,我們就可以解放了,簡單粗暴。
\(^o^)/~
下面是常用的創建上傳任務的方法:
/* 回調類型,這是我爲了排版方便抽出來的,實際框架中沒有 */
typedef void (^UploadCompletionBlock)(NSData*,NSURLReponse*,NSError*);
/* 創建上傳任務,需要提供上傳文件二進制數據 */
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request
fromData:(NSData *)bodyData
completionHandler:(UploadCompletionBlock)completion;
/* 創建上傳任務,需要提供上傳文件所在的URL路徑,不過這個方法常配合“PUT”請求使用 */
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request
fromFile:(NSURL *)fillURL
completionHandler:(UploadCompletionBlock)completion;
下面是上傳實例:
- (void) NSURLSessionBinaryUploadTaskTest {
// 1.創建url,採用Apache本地服務器進行測試
NSString *urlStr = @"http://localhost/upload.php";
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:urlStr];
// 2.創建請求,這裏要設置POST請求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";// 文件上傳使用post
// 3.獲取全局會話Session
NSURLSession *session = [NSURLSession sharedSession];
// 4.創建上傳任務,Request的Body Data將被忽略,而由fromData提供
NSData *data = [NSData dataWithContentsOfFile:@"/Users/userName/Desktop/IMG_0359.jpg"];
NSURLSessionUploadTask *upload =
[session uploadTaskWithRequest:request
fromData:data
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error == nil) {
NSString *result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"upload success:%@",result);
} else {
NSLog(@"upload error:%@",error);
}
}]
// 5.啓動任務
[upload resume];
}
是不是很簡單,數據請求、文件下載、文件上傳基本上都差不多,使用起來比NSURLConnection
方便多了,還有什麼理由不用NSURLSession
呢!!
4.用dataTask上傳文件【閒得蛋疼可以試一下】
除了上面的上傳方式,實際上你也可以用NSURLSessionDataTask
的方式上傳,不過你就要自己設置上傳BodyData和Header了,具體構建細節可以參考iOS學習筆記12-網絡請求(一)NSURLConnection裏面的構建過程,這裏給個參考吧:
#pragma mark 上傳文件
-(void)uploadFile{
NSString *fileName = @"pic.jpg";
//1.創建url
NSString *urlStr = @"http://192.168.1.208/FileUpload.aspx";
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:urlStr];
//2.創建請求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
//3.構建上傳表單數據
//設置數據體
NSData *data = [self getHttpBody:fileName];
request.HTTPBody = data;
//設置請求頭
NSString *lengthStr = [NSString stringWithFormat:@"%lu",(unsigned long)data.length];
[request setValue:lengthStr forHTTPHeaderField:@"Content-Length"];
NSString *typeStr = [NSString stringWithFormat:@"multipart/form-data; boundary=%@",kBOUNDARY_STRING];
[request setValue:typeStr forHTTPHeaderField:@"Content-Type"];
//4.創建會話
NSURLSession *session = [NSURLSession sharedSession];
//5.創建dataTask任務,去做上傳的功能
NSURLSessionDataTask *uploadTask = [session dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error) {
NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@",dataStr);
}else{
NSLog(@"error is :%@",error.localizedDescription);
}
}];
//6.啓動任務
[uploadTask resume];
}
上面的獲取數據體方法getHttpBody,我也貼過來了
#pragma mark 取得數據體
-(NSData *)getHttpBody:(NSString *)fileName{
NSMutableData *dataM = [NSMutableData data];
NSString *type = [self getMIMETypes:fileName];
//構建請求體body的頂部
NSMutableString *bodyTop = [NSMutableString string];
//宏kBOUNDARY_STRING就是boundary標示
[bodyTop appendFormat:@"--%@\n",kBOUNDARY_STRING];
[bodyTop appendFormat:@"Content-Disposition: form-data; name=\"file1\"; filename=\"%@\"\n",fileName];
[bodyTop appendFormat:@"Content-Type: %@\n\n",type];
//構建請求體body的底部
NSString *bodyBottom = [NSString stringWithFormat:@"\n--%@--",kBOUNDARY_STRING];
NSString *filePath = [[NSBundle mainBundle] pathForResource:fileName ofType:nil];
//構建請求體body中間的二進制上傳數據
NSData *fileData = [NSData dataWithContentsOfFile:filePath];
//把頂部、數據、底部組合起來,形成body
[dataM appendData:[bodyTop dataUsingEncoding:NSUTF8StringEncoding]];
[dataM appendData:fileData];
[dataM appendData:[bodyBottom dataUsingEncoding:NSUTF8StringEncoding]];
return dataM;
}
三、會話Session控制
上面我們都是使用的全局的NSURLSession
,一般情況下我們就夠用,但如果遇到兩個連接使用不同的資源配置的情況,怎麼辦?答案就是自己定製。
- NSURLSession支持我們自己定製NSURLSession
- NSURLSession支持的三種會話配置:
defaultSessionConfiguration
:
進程內會話(默認會話),用硬盤來緩存數據。ephemeralSessionConfiguration
:
臨時的進程內會話(內存),不會將cookie、緩存儲存到本地,只會放到內存中,當應用程序退出後數據也會消失。backgroundSessionConfiguration
:
後臺會話,相比默認會話,該會話會在後臺開啓一個線程進行網絡數據處理。
下面就是定製NSURLSession的過程:
//使用默認會話配置
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 5.0f;//請求超時時間
sessionConfig.allowsCellularAccess = true;//是否允許蜂窩網絡下載(2G/3G/4G)
//創建會話,指定配置和代理
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
上面設置了代理,NSURLSession有很多代理協議:
NSURLSessionDelegate
:NSObject
會話父協議NSURLSessionTaskDelegate
:NSURLSessionDelegate
任務協議NSURLSessionDataDelegate
:NSURLSessionTaskDelegate
數據協議NSURLSessionDownloadDelegate
:NSURLSessionTaskDelegate
下載協議NSURLSessionStreamDelegate
:NSURLSessionTaskDelegate
網絡流協議
下面就拿最常用的下載協議NSURLSessionDownloadDelegate
來講下:
/* 下載中(會多次調用,可以記錄下載進度) */
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask /* 下載任務 */
didWriteData:(int64_t)bytesWritten /* 這次下載完成的字節數 */
totalBytesWritten:(int64_t)totalBytesWritten /* 已經下載完成的總字節數 */
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite; /* 需要下載完成的總字節數 */
/* 成功下載完成 */
-(void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask /* 下載任務 */
didFinishDownloadingToURL:(NSURL *)location;/* 下載完成後臨時存放的URL */
/* 任務完成,不管是否下載成功 */
-(void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task /* 下載任務 */
didCompleteWithError:(NSError *)error;/* 錯誤 */
實際上NSURLSessionTask
任務除了resume
啓動之外,還有一些方法
/* 取消任務 */
- (void)cancel;
/* 掛起任務(暫停任務) */
- (void)suspend;
/* 啓動任務 */
- (void)resume;
下面來個代碼總結:
-(void)downloadFile{
NSString *fileName = _textField.text;
NSString *urlStr = [NSString stringWithFormat: @"http://192.168.1.208/FileDownload.aspx?file=%@",fileName];
urlStr = [urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:urlStr];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 5.0f;//請求超時時間
sessionConfig.allowsCellularAccess = true;//是否允許蜂窩網絡下載(2G/3G/4G)
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
_downloadTask = [session downloadTaskWithRequest:request];
[_downloadTask resume];
}
#pragma mark 點擊取消下載
-(void)cancelDownload{
[_downloadTask cancel];
}
#pragma mark 點擊掛起下載
-(void)suspendDownload{
[_downloadTask suspend];
}
#pragma mark 點擊恢復下載
-(void)resumeDownload{
[_downloadTask resume];
}
#pragma mark - 下載任務代理
#pragma mark 下載中(會多次調用,可以記錄下載進度)
-(void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
[self setUIStatus:totalBytesWritten expectedToWrite:totalBytesExpectedToWrite];//設置界面狀態
}
#pragma mark 下載完成
-(void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
NSError *error;
NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
NSString *savePath = [cachePath stringByAppendingPathComponent:_textField.text];
NSURL *saveUrl = [NSURL fileURLWithPath:savePath];
[[NSFileManager defaultManager] copyItemAtURL:location toURL:saveUrl error:&error];
}
#pragma mark 任務完成,不管是否下載成功
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
[self setUIStatus:0 expectedToWrite:0];//設置界面狀態
}
四、Session後臺開啓任務
NSURLSession
支持程序的後臺下載和上傳,蘋果官方將其稱爲進程之外的上傳和下載,這些任務都是交給後臺守護線程完成的,而非應用程序本身。
即使文件在下載和上傳過程中崩潰了也可以繼續運行(注意如果用戶強制退關閉應用程序,NSURLSession會斷開連接
)。
我們先來看下如何創建一個後臺Session
#pragma mark 取得一個後臺會話(保證一個後臺會話,這通常很有必要,這裏採用單例模式的形式)
- (NSURLSession *)backgroundSession{
static NSURLSession *session = nil;
static dispatch_once_t token;//下面代碼塊只執行一次,以後都不執行
dispatch_once(&token, ^{
NSStirng *identifier = @"com.cmjstudio.URLSession";
NSURLSessionConfiguration *sessionConfig =
[NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:identifier];
sessionConfig.timeoutIntervalForRequest = 5.0f;//請求超時時間
sessionConfig.discretionary = YES;//系統自動選擇最佳網絡下載
sessionConfig.HTTPMaximumConnectionsPerHost = 5;//限制每次最多5個連接
//創建會話
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
});
return session;
}
然後我們拿到這個後臺Session就可以做上面我們講的下載和上傳任務了。
我們來了解下程序進入後臺後,任務是如何調度的,先上圖:
當程序進入後臺後,事實上任務是交給iOS系統來調度的,具體什麼時候下載完成就不得而知,例如有個較大的文件經過一個小時下載完了,正常打開應用程序看到的此文件下載進度應該在100%的位置,但是由於程序已經在後臺無法更新程序UI,而此時可以通過應用程序代理方法進行UI更新。
在AppDelegate.m中添加以下函數:
/*
有其中幾個任務完成後,系統會調用此應用程序的該方法
此方法會包含一個competionHandler,通常我們會保存此對象
competionHandler此操作表示應用完成所有處理工作
*/
- (void)application:(UIApplication *)application
handleEventsForBackgroundURLSession:(NSString *)identifier
completionHandler:(void (^)())completionHandler
{
//backgroundSessionCompletionHandler是自定義的一個屬性
self.backgroundSessionCompletionHandler = completionHandler;
}
在XXSession.m文件中實現NSURLSessionDelegate
代理方法:
/*
直到最後一個任務完成,系統會調用該方法。
在這個方法中通常可以進行UI更新,並調用completionHandler通知系統已經完成所有操作。
*/
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
//這中間就可以寫更新UI的代碼了,code
if (appDelegate.backgroundSessionCompletionHandler) {
void (^completionHandler)() = appDelegate.backgroundSessionCompletionHandler;
appDelegate.backgroundSessionCompletionHandler = nil;
completionHandler();
}
}