iOS學習筆記13-網絡(二)NSURLSession

在2013年WWDC上蘋果揭開了NSURLSession的面紗,將它作爲NSURLConnection的繼任者。現在使用最廣泛的第三方網絡框架:AFNetworking、SDWebImage等等都使用了NSURLSession。作爲iOS開發人員,應該緊隨蘋果的步伐,不斷的學習,無論是軟件的更新、系統的更新、API的更新,而不能墨守成規。
* 相比較NSURLConnection,NSURLSession提供了 配置會話緩存、協議、cookie和證書能力,這使得網絡架構和應用程序可以獨立工作、互不干擾。
* 另外,NSURLSession另一個重要的部分是 會話任務,它負責加載數據,在客戶端和服務器端進行文件的上傳下載。

下面讓我們正式進入NSURLSession學習。

一、NSURLSession介紹

NSURLSession類結構圖

在NSURLSession時代,網絡請求基本上由3個任務完成:
  • NSURLSessionData:請求數據任務
  • NSURLSessionUploadTask:請求上傳任務
  • NSURLSessionDownloadTask:請求下載任務
關係圖如下:

NSURLSessionTask關係圖

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支持的三種會話配置:
    1. defaultSessionConfiguration
      進程內會話(默認會話),用硬盤來緩存數據。
    2. ephemeralSessionConfiguration
      臨時的進程內會話(內存),不會將cookie、緩存儲存到本地,只會放到內存中,當應用程序退出後數據也會消失。
    3. 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有很多代理協議:
  • NSURLSessionDelegateNSObject
    會話父協議
  • NSURLSessionTaskDelegateNSURLSessionDelegate
    任務協議
  • NSURLSessionDataDelegateNSURLSessionTaskDelegate
    數據協議
  • NSURLSessionDownloadDelegateNSURLSessionTaskDelegate
    下載協議
  • NSURLSessionStreamDelegateNSURLSessionTaskDelegate
    網絡流協議
下面就拿最常用的下載協議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();  
    }
}
如果喜歡我的文章,請關注我O(∩_∩)O哈!,求土豪打賞!有問題也可以提出來!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章