iOS網絡NSURLSession使用詳解

1、總體介紹

  • NSURLSession在2013年隨着iOS7的發佈一塊兒面世,蘋果對它的定位是做爲NSURLConnection的替代者,而後逐步將NSURLConnection退出歷史舞臺。如今使用最普遍的第三方網絡框架:AFNetworking、SDWebImage等等都使用了NSURLSession。做爲iOS開發人員,應該緊隨蘋果的步伐,不斷的學習,不管是軟件的更新、系統的更新、API的更新,而不能墨守成規。php

  • Session翻譯爲中文意思是會話,咱們知道,在七層網絡協議中有物理層->數據鏈路層->網絡層->傳輸層->會話層->表示層->應用層,那咱們能夠將NSURLSession類理解爲會話層,用於管理網絡接口的建立、維護、刪除等等工做,咱們要作的工做也只是會話層以後的層便可,底層的工做NSURLSession已經幫咱們封裝好了。html

  • 另外還有一些Session,好比AVAudioSession用於音視頻訪問,WCSession用於WatchOS通信,它們都是創建一個會話,並管理會話,封裝一些底層,方便咱們使用。觸類旁通。json

2、使用的通常步驟

其核心就是對網絡任務進行封裝,實現多線程。好比將一個網絡請求交給NSURLSession,最後NSURLSession將訪問結果經過block回調返回,期間自動實現多線程,並且能夠經過代理實現監聽(是否成功,當前的進度等等); 大體分爲3個步驟:緩存

NSURL:請求地址,定義一個網絡資源路徑:

NSURL *url = [NSURL URLWithString:@"協議://主機地址/路徑?參數&參數"];

解釋以下: 服務器

  • 協議:不一樣的協議,表明着不一樣的資源查找方式、資源傳輸方式,好比經常使用的http,ftp等
  • 主機地址:存放資源的主機的IP地址(域名)
  • 路徑:資源在主機中的具體位置
  • 參數:參數無關緊要,也能夠多個。若是帶參數的話,用「?」號後面接參數,多個參數的話之間用&隔開

NSURLRequest:請求,根據前面的NSURL創建一個請求:

NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:30.0];

參數解釋以下: cookie

  • url:資源路徑
  • cachePolicy:緩存策略(不管使用哪一種緩存策略,都會在本地緩存數據),類型爲美劇類型,取值以下:
    • NSURLRequestUseProtocolCachePolicy = 0 //默認的緩存策略,使用協議的緩存策略
    • NSURLRequestReloadIgnoringLocalCacheData = 1 //每次都從網絡加載
    • NSURLRequestReturnCacheDataElseLoad = 2 //返回緩存不然加載,不多使用
    • NSURLRequestReturnCacheDataDontLoad = 3 //只返回緩存,沒有也不加載,不多使用
  • timeoutInterval:超時時長,默認60s,這裏設置爲30s

另外,還能夠設置其它一些信息,好比請求頭,請求體等等,以下:網絡

注意,下面的request應爲NSMutableURLRequest,便可變類型session

// 告訴服務器數據爲json類型
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; 
// 設置請求體(json類型)
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:@{@"userid":@"123456"} options:NSJSONWritingPrettyPrinted error:nil];
request.HTTPBody = jsonData; 

NSURLSession:建立NSURLSession發送請求

  • 爲了方便使用,蘋果提供了一個全局的NSURLSession單例,如同NSURLConnection同樣。這樣作的缺陷就是不能監控,若是想要監控每個請求,則必須經過代理來監聽,咱們知道單例是一對多的,而代理是一對一,所以必須本身實例化單獨的Session任務對象(NSURLConnection則很難),來實現單獨監控。
  • 系統一共提供了5種任務類,繼承關係以下圖所示。其中NSURLSessionTask爲抽象類,不能實現網絡訪問,NSURLSessionStreanTask(以流的方式進行網絡訪問)使用的比較少.使用的多的是dataTask、downloadTask、uploadTask,即圖中紅色框框圈的部分,基本知足了網絡訪問的基本需求:獲取數據(一般是JSON、XML等)、文件上傳、文件下載。這三個類都是NSURLSessionTask這個抽象類的子類,相比直接使用NSURLConnection,NSURLSessionTask支持任務的暫停、取消和恢復,而且默認任務運行在其餘非主線程中。 
  • 根據圖中代理協議的名字不難發現,每個任務類都有相對應的代理協議,只有NSURLSessionUploadTask沒有對應的代理協議,由於NSURLSessionUploadTask繼承自NSURLSessionDataTask,所以NSURLSessionDataDelegate即爲NSURLSessionUploadTask對應的代理協議。

       

三 舉例

1.NSURLSession請求網絡數據

下面以蘋果提供的全局NSURLSession單例爲例,代碼以下:多線程

複製代碼
/// 向網絡請求數據
- (void)NSURLSessionTest {
    // 1.建立url
    // 請求一個網頁
    NSString *urlString = @"http://www.cnblogs.com/mddblog/p/5215453.html";
// 一些特殊字符編碼
    urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
    NSURL *url = [NSURL URLWithString:urlString];
    
    // 2.建立請求 並:設置緩存策略爲每次都從網絡加載 超時時間30秒
    NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:30];

    // 3.採用蘋果提供的共享session
    NSURLSession *sharedSession = [NSURLSession sharedSession];
    
    // 4.由系統直接返回一個dataTask任務
    NSURLSessionDataTask *dataTask = [sharedSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        // 網絡請求完成以後就會執行,NSURLSession自動實現多線程
        NSLog(@"%@",[NSThread currentThread]);
        if (data && (error == nil)) {
            // 網絡訪問成功
            NSLog(@"data=%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
        } else {
            // 網絡訪問失敗
            NSLog(@"error=%@",error);
        }
    }];
    
    // 5.每個任務默認都是掛起的,須要調用 resume 方法
    [dataTask resume];
}
複製代碼

2.NSURLSession文件下載

複製代碼
/// 文件下載
- (void)NSURLSessionDownloadTaskTest {
    // 1.建立url
    NSString *urlString = [NSString stringWithFormat:@"http://localhost/周杰倫 - 楓.mp3"];
    // 一些特殊字符編碼
    urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
    NSURL *url = [NSURL URLWithString:urlString];
    
    // 2.建立請求
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    
    // 3.建立會話,採用蘋果提供全局的共享session
    NSURLSession *sharedSession = [NSURLSession sharedSession];
    
    // 4.建立任務
    NSURLSessionDownloadTask *downloadTask = [sharedSession downloadTaskWithRequest:request completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error == nil) {
            // location:下載任務完成以後,文件存儲的位置,這個路徑默認是在tmp文件夾下!
            // 只會臨時保存,所以須要將其另存
            NSLog(@"location:%@",location.path);
            
            // 採用模擬器測試,爲了方便將其下載到Mac桌面
//            NSString *filePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
            NSString *filePath = @"/Users/userName/Desktop/周杰倫 - 楓.mp3";
            NSError *fileError;
            [[NSFileManager defaultManager] copyItemAtPath:location.path toPath:filePath error:&fileError];
            if (fileError == nil) {
                NSLog(@"file save success");
            } else {
                NSLog(@"file save error: %@",fileError);
            }
        } else {
            NSLog(@"download error:%@",error);
        }
    }];
    
    // 5.開啓任務
    [downloadTask resume];
}
複製代碼

3.NSURLSession文件上傳

3.1 採用uploadTask任務,以數據流的方式進行上傳
這種方式好處就是大小不受限制,上傳須要服務器端腳本支持,腳本源代碼見本文檔最後的附錄,客戶端示例代碼以下:app

複製代碼
/// 以流的方式上傳,大小理論上不受限制,但應注意時間
- (void) NSURLSessionBinaryUploadTaskTest {
    // 1.建立url  採用Apache本地服務器
    NSString *urlString = @"http://localhost/upload.php";
    urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLFragmentAllowedCharacterSet]];
    NSURL *url = [NSURL URLWithString:urlString];
    
    // 2.建立請求
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    // 文件上傳使用post
    request.HTTPMethod = @"POST";
    
    // 3.開始上傳   request的body data將被忽略,而由fromData提供
    [[[NSURLSession sharedSession] uploadTaskWithRequest:request fromData:[NSData dataWithContentsOfFile:@"/Users/userName/Desktop/IMG_0359.jpg"]     completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error == nil) {
            NSLog(@"upload success:%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
        } else {
            NSLog(@"upload error:%@",error);
        }
    }] resume];
}
複製代碼

3.2 採用dataTask任務,拼接表單的方式進行上傳

  • 上傳的關鍵是請求體部分的表單拼接,獲取本地上傳文件的類型(MIME Types),至於具體的網絡上傳則很簡單。 另外拼接表單的方式會有大小限制,即HTML的MAX_FILE_SIZE限制(能夠本身設定,通常2MB)。

  • 根據上面的繼承關係圖,咱們知道uploadTask是dataTask的子類,也可使用uploadTask來代替dataTask。在代碼示例中4.2步驟徹底能夠替換4.1步驟。這時,uploadTaskWithRequest函數的fromData無關緊要,文件已在request裏面包含。

注意:然而在蘋果官方對uploadTaskWithRequest函數的介紹:request的body data in this request object are ignored,會被忽略,而測試時發現沒有被忽略,且request必須包含HTTPBody,反而fromData被忽略。那麼暫時理解爲蘋果對uploadTaskWithRequest函數的使用時沒有考慮拼接表單的方式,那麼當咱們使用拼接表單時,建議不要使用uploadTask,雖然這樣也能成功

  • 服務器端用到的upload.php源代碼見本文最後的附錄

表單拼接格式以下,boundary做爲分界線: 

複製代碼
--boundary
Content-Disposition:form-data;name=」表單控件名稱」;filename=」上傳文件名稱」
Content-Type:要上傳文件MIME Types

要上傳文件二進制數據;

--boundary--
複製代碼

拼接表單示例代碼:

複製代碼
/// 文件上傳
- (void)NSURLSessionUploadTaskTest {
    // 1.建立url  採用Apache本地服務器
    NSString *urlString = @"http://localhost/upload/upload.php";
    urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLFragmentAllowedCharacterSet]];
    NSURL *url = [NSURL URLWithString:urlString];
    
    // 2.建立請求
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    // 文件上傳使用post
    request.HTTPMethod = @"POST";
    NSString *contentType = [NSString stringWithFormat:@"multipart/form-data; boundary=%@",@"boundary"];
    [request setValue:contentType forHTTPHeaderField:@"Content-Type"];
    // 3.拼接表單,大小受MAX_FILE_SIZE限制(2MB)  FilePath:要上傳的本地文件路徑  formName:表單控件名稱,應於服務器一致
    NSData* data = [self getHttpBodyWithFilePath:@"/Users/userName/Desktop/IMG_0359.jpg" formName:@"file" reName:@"newName.png"];
    request.HTTPBody = data;
    // 根據須要是否提供,非必須,若是不提供,session會自動計算
    [request setValue:[NSString stringWithFormat:@"%lu",data.length] forHTTPHeaderField:@"Content-Length"];

    // 4.1 使用dataTask
    [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error == nil) {
            NSLog(@"upload success:%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
        } else {
            NSLog(@"upload error:%@",error);
        }
        
    }] resume];
#if 0
    // 4.2 開始上傳 使用uploadTask   fromData:無關緊要,會被忽略
    [[[NSURLSession sharedSession] uploadTaskWithRequest:request fromData:nil     completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (error == nil) {
            NSLog(@"upload success:%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
        } else {
            NSLog(@"upload error:%@",error);
        }
    }] resume];
#endif
}

/// filePath:要上傳的文件路徑   formName:表單控件名稱  reName:上傳後文件名
- (NSData *)getHttpBodyWithFilePath:(NSString *)filePath formName:(NSString *)formName reName:(NSString *)reName
{
    NSMutableData *data = [NSMutableData data];
    NSURLResponse *response = [self getLocalFileResponse:filePath];
    // 文件類型:MIMEType  文件的大小:expectedContentLength  文件名字:suggestedFilename
    NSString *fileType = response.MIMEType;
    
    // 若是沒有傳入上傳後文件名稱,採用本地文件名!
    if (reName == nil) {
        reName = response.suggestedFilename;
    }
    
    // 表單拼接
    NSMutableString *headerStrM =[NSMutableString string];
    [headerStrM appendFormat:@"--%@\r\n",@"boundary"];
    // name:表單控件名稱  filename:上傳文件名
    [headerStrM appendFormat:@"Content-Disposition: form-data; name=%@; filename=%@\r\n",formName,reName];
    [headerStrM appendFormat:@"Content-Type: %@\r\n\r\n",fileType];
    [data appendData:[headerStrM dataUsingEncoding:NSUTF8StringEncoding]];
    
    // 文件內容
    NSData *fileData = [NSData dataWithContentsOfFile:filePath];
    [data appendData:fileData];
    
    NSMutableString *footerStrM = [NSMutableString stringWithFormat:@"\r\n--%@--\r\n",@"boundary"];
    [data appendData:[footerStrM  dataUsingEncoding:NSUTF8StringEncoding]];
//    NSLog(@"dataStr=%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
    return data;
}
/// 獲取響應,主要是文件類型和文件名
- (NSURLResponse *)getLocalFileResponse:(NSString *)urlString
{
    urlString = [urlString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLFragmentAllowedCharacterSet]];
    // 本地文件請求
    NSURL *url = [NSURL fileURLWithPath:urlString];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    
    __block NSURLResponse *localResponse = nil;
    // 使用信號量實現NSURLSession同步請求
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        localResponse = response;
        dispatch_semaphore_signal(semaphore);
    }] resume];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    return  localResponse;
}
複製代碼

四 NSURLSessionConfiguration


NSURLConnection是全局性的,即它的配置對全局有效,若是有兩個連接須要不一樣的cookies、證書這些公共資源,則NSURLConnection沒法知足要求,這時NSURLSession的優點則體現出來,NSURLSession能夠同過NSURLSessionConfiguration能夠設置全局的網絡訪問屬性。

NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
// delegateQueue:請求完成回調函數和代理函數的運行線程,若是爲nil則系統自動建立一個串行隊列,不影響sessionTask的運行線程
NSURLSession *session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];

三種會話方式: 

  1. defaultSessionConfiguration:進程內會話(默認會話),相似 NSURLConnection的標準配置,用硬盤來緩存數據。
  2. ephemeralSessionConfiguration:臨時的進程內會話(內存),不會將cookie、緩存儲存到本地,只會放到內存中,當應用程序退出後數據也會消失,能夠用於實現「祕密瀏覽」
  3. backgroundSessionConfiguration:創建後臺會話能夠在應用程序掛起,退出,崩潰的狀況下運行上傳和下載任務,後臺另起一個線程。另外,系統會根據設備的負載程度決定分配下載的資源,所以有可能會很慢甚至超時失敗。

設置一些網絡屬性:

  • HTTPAdditionalHeaders:能夠設置出站請求的數據頭
  • configuration.HTTPAdditionalHeaders = @{
        @"Accept": @"application/json",  
        @"Accept-Language": @"en",
        @"Authorization": authString,
        @"User-Agent": userAgentString
    };
  • networkServiceType,設置網絡服務類型
    • NSURLNetworkServiceTypeDefault 默認
    • NSURLNetworkServiceTypeVoIP VoIP
    • NSURLNetworkServiceTypeVideo 視頻
    • NSURLNetworkServiceTypeBackground 後臺
    • NSURLNetworkServiceTypeVoice 語音
  • allowsCellularAccess:容許蜂窩訪問
  • timeoutIntervalForRequest:請求的超時時長
  • requestCachePolicy:緩存策略

注意事項:若是是自定義會話並指定了代理,會話會對代理進行強引用,在視圖控制器銷燬以前,須要取消網絡會話,不然會形成內存泄漏

 

附錄——服務器端文件上傳PHP源代碼

  • 以表單形式上傳,能夠獲取文件名等等信息,注意images文件夾的權限應爲全部用戶可讀寫:
複製代碼
<?php
    header("Content-type: application/json; charset=utf-8");
    // 配置文件須要上傳到服務器的路徑,須要容許全部用戶有可寫權限,不然沒法上傳!
    $uploaddir = 'images/';
    // file表單名稱,應與客戶端一致
    $uploadfile = $uploaddir . basename($_FILES['file']['name']);
    
    move_uploaded_file($_FILES['file']['tmp_name'], $uploadfile);
    
    echo json_encode($_FILES);
?>
複製代碼
  • 以文件流的形式上傳文件,文件的名字沒有動態獲取,而是直接命名,注意images文件夾的權限應爲全部用戶可讀寫
複製代碼
<?php
    /** 二進制流生成文件
     * $_POST 沒法解釋二進制流,須要用到 $GLOBALS['HTTP_RAW_POST_DATA'] 或 php://input
     * $GLOBALS['HTTP_RAW_POST_DATA'] 和 php://input 都不能用於 enctype=multipart/form-data
     * @param    String  $file   要生成的文件路徑
     * @return   boolean
     */
    function binary_to_file($file){
        $content = $GLOBALS['HTTP_RAW_POST_DATA'];          // 須要php.ini設置
        if(empty($content)){
            $content = file_get_contents('php://input');    // 不須要php.ini設置,內存壓力小
        }
        $ret = file_put_contents($file, $content, true);
        return $ret;
    }
    $file_dir="images/image.png";  // 固定的文件名,注意設置images文件夾權限爲全部用戶可讀寫!!!
    binary_to_file($file_dir);
?>
複製代碼
相關文章
相關標籤/搜索