1. 程式人生 > >NSURLSession-介紹、上傳、下載(2)

NSURLSession-介紹、上傳、下載(2)

NSURLConnection在iOS9被宣佈棄用,NSURLSession從13年發展到現在,終於迎來了它獨步江湖的時代.NSURLSession是蘋果在iOS7後為HTTP資料傳輸提供的一系列介面,比NSURLConnection強大,坑少。
一、NSURLSession的簡介
1.NSURLSession的建立
(1)使用shareSession返回session的單例,建立會話物件
NSURLSession *session = [NSURLSession sharedSession];
(2)使用NSURLSessionConfiguration來配置session,NSURLSessionConfiguration物件用於初始化NSURLSession物件,展開請求級別中與NSMutableURLRequet相關的方案NSURLSessionConfiguration對於會話如何產生請求,提供了相當多的控制和靈活性。從網路訪問效能,到cookie,安全性,快取策略,自定義協議,啟動事件設定,以及用於移動裝置優化的幾個新屬性,你會發現你一直在尋找的,正是NSURLSessionConfiguration。配置在初始化時被讀取一次,之後都是不會變化的。

       + defaultSessionConfiguration返回標準配置,這實際上與NSURLConnection的網路協議棧是一樣的,具有相同的共享NSHTTPCookieStorage,共享NSURLCache和共享NSURLCredentialStorage。
      + ephemeralSessionConfiguration返回一個預設配置,沒有永續性儲存的快取,Cookie或證書。這對於實現像祕密瀏覽功能的功能來說,是很理想的。
       + backgroundSessionConfiguration:獨特之處在於,它會建立一個後臺會話。後臺會話不同於常規的,普通的會話,它甚至可以在應用程式掛起,退出,崩潰的情況下執行上傳和下載任務。初始化時指定的識別符號,被用於向任何可能在程序外恢復後臺傳輸的守護程序提供上下文。

建立和配NSURLSession的示例程式碼如下:

//預設型別的
    NSURLSessionConfiguration * defaultConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
    //即時型別的
    NSURLSessionConfiguration * ephemeralConfiguration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
    //後臺型別的
    NSURLSessionConfiguration * backgroundConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"SessionId"
]; //建立並設定session NSURLSession * defaultSession = [NSURLSession sessionWithConfiguration:defaultConfiguration]; NSURLSession * ephemeralSession = [NSURLSession sessionWithConfiguration:ephemeralConfiguration]; NSURLSession * backgroundSession = [NSURLSession sessionWithConfiguration:backgroundConfiguration];

(3)NSURLSession的優勢
*它支援http2.0
*支援後臺上傳/下載,下載時是多執行緒非同步處理,處理任務時直接把資料下載到磁碟上
*提供全域性的session並且可以統一配置
*同一個session傳送多個請求,只需建立一次連線(複用TCP)

2、NSURLSessionTask
NSURLSessionTask是一個抽象類,使用時應該使用它的子類;NSURLSessionTask有兩個子類,
(1)NSURLSessionDataTask,可以用來處理一般的網路請求,如GET、POST,它的子類NSURLSessionUploadTask多用來處理上傳請求。
(2)NSURLSessionDownloadTask,主要用來處理下載請求。
3、下載任務
(1)NSURLConnection檔案下載
對於小檔案的下載,直接使用sendAsynchronousRequest:queue:completionHandler:傳送一個非同步的get請求,回撥的data就是下載的內容,它放在記憶體中;這種下載方式簡單,是一次性將下載內容下載完,適用於小檔案的下載。如果下載大檔案,一次性下載完,記憶體會爆。
NSURLConnection下載大檔案主要是使用它的代理方法,其中didReceiveData代理方法用來接受資料,他會被頻繁呼叫,每次傳回來一部分data,我們只需定義一個全域性的NSMutableData(在didReceiveResponse接受到響應時初始化),在didReceiveData中拼接,最後在connectionDidFinishLoading中將整個NSMutableData寫入到沙盒中。
注意:通常大檔案下載是需要給使用者展示下載進度的。
這個數值是: 已經下載的資料大小/要下載的檔案總大小。已經下載的資料我們可以記錄,要下載的檔案總大小在伺服器返回的響應頭裡面可以拿到,在接受到響應的方法裡執行

NSHTTPURLResponse *res = (NSHTTPURLResponse*)response;

    NSDictionary *headerDic = res.allHeaderFields;
    NSLog(@"%@",headerDic);
    self.fileLength = [[headerDic objectForKey:@"Content-Length"] intValue];

不得不說蘋果太為開發者考慮了,我們不必這麼麻煩的去獲取檔案總大小了,
response.expectedContentLength 這句程式碼就搞定了。
response.suggestedFilename 這句代表獲取下載的檔名

但是,上述的處理方法會出現記憶體問題,應為用來接受檔案的NSmutableData一直在記憶體中,所以隨著檔案的下載記憶體會一直變大,解決的方法是獲取到一部分data時就寫入到沙盒,然後釋放記憶體中的data

斷點下載

NSURLConnection 提供了一個cancel方法,這並不是暫停,而是取消下載任務。如果要實現斷點下載必須要了解HTTP協議中請求頭的Range。
這裡寫圖片描述
不難看出,通過設定請求頭的Range我們可以指定下載的位置、大小。
那麼我們這樣設定bytes=500- 從500位元組以後的所有位元組,
只需要在didReceiveData中記錄已經寫入沙盒中檔案的大小(self.currentLength),把這個大小設定到請求頭中,因為第一次下載肯定是沒有執行過didReceive方法,self.currentLength也就為0,也就是從頭開始下。

#pragma mark --按鈕點選事件

- (IBAction)btnClicked:(UIButton *)sender {

    // 狀態取反
    sender.selected = !sender.isSelected;

    // 斷點續傳
    // 斷點下載

    if (sender.selected) { // 繼續(開始)下載
        // 1.URL
        NSURL *url = [NSURL URLWithString:@"http://localhost:8080//term_app/hdgg.zip"];

        // 2.請求
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];

        // 設定請求頭
        NSString *range = [NSString stringWithFormat:@"bytes=%lld-", self.currentLength];
        [request setValue:range forHTTPHeaderField:@"Range"];

        // 3.下載
        self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
    } else { // 暫停

        [self.connection cancel];
        self.connection = nil;
    }

在下載過程中,為了提高效率,充分利用cpu效能,通常會執行多執行緒下載,程式碼就不貼了,分析一下思路:
下載開始,建立一個和要下載的檔案大小相同的檔案(如果要下載的檔案為100M,那麼就在沙盒中建立一個100M的檔案,然後計算每一段的下載量,開啟多條執行緒下載各段的資料,分別寫入對應的檔案部分)。

(2)NSURLSession下載
使用NSURLSessionDownload不需要考慮邊下載,邊寫入沙盒的問題了,因為蘋果為我們做好了,呼叫downloadTaskWithURL:completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error)即可。

NSURL* url = [NSURL URLWithString:@"http://dlsw.baidu.com/sw-search-sp/soft/9d/25765/sogou_mac_32c_V3.2.0.1437101586.dmg"];

    // 得到session物件
    NSURLSession* session = [NSURLSession sharedSession];

    // 建立任務
    NSURLSessionDownloadTask* downloadTask = [session downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {

    }];
    // 開始任務
    [downloadTask resume];

回撥中的location是下載好的檔案寫入到沙盒temp中的地址,因為temp中的檔案會自動刪除,所以我們要在回撥中把檔案移到cache中。

NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
        // response.suggestedFilename : 建議使用的檔名,一般跟伺服器端的檔名一致
        NSString *file = [caches stringByAppendingPathComponent:response.suggestedFilename];

        // 將臨時檔案剪下或者複製Caches資料夾
        NSFileManager *mgr = [NSFileManager defaultManager];

        // AtPath : 剪下前的檔案路徑
        // ToPath : 剪下後的檔案路徑
        [mgr moveItemAtPath:location.path toPath:file error:nil];

缺點:不過通過這種方式下載有個缺點就是無法監聽下載進度,要監聽下載進度,蘋果通常的作法是通過delegate,要遵
- (void)URLSession:(NSURLSession )session downloadTask:(NSURLSessionDownloadTask )downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite方法可以監聽下載進度

#pragma mark -- NSURLSessionDownloadDelegate
/**
 *  下載完畢會呼叫
 *
 *  @param location     檔案臨時地址
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
}
/**
 *  每次寫入沙盒完畢呼叫
 *  在這裡面監聽下載進度,totalBytesWritten/totalBytesExpectedToWrite
 *
 *  @param bytesWritten              這次寫入的大小
 *  @param totalBytesWritten         已經寫入沙盒的大小
 *  @param totalBytesExpectedToWrite 檔案總大小
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
      didWriteData:(int64_t)bytesWritten
 totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
    self.pgLabel.text = [NSString stringWithFormat:@"下載進度:%f",(double)totalBytesWritten/totalBytesExpectedToWrite];
}

/**
 *  恢復下載後呼叫,
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
 didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes
{

}

斷點續傳
用NSURLSessionDownloadTask做斷點下載也很簡單,我們先了解一下任務的取消方法
斷點續傳—
斷點續傳涉及到的類和方法
  NSURLSessionDownloadTask:
  - (void) suspend; 暫停 ,可以恢復
  - (void) cancel; 取消,不可以恢復
  - (void) cancelByProducingResumeData:^(NSData * _Nullable resumeData) : ; 取消的任務
  - (void) resume; 在建立新的任務下resume,相當於重新啟動任務

注意:(1)如果使用suspend方法暫停下載,因為是可恢復的,那麼對應的下載任務物件是唯一的。使用的時候suspend要和resume成對使用,都是同一個NSURLSessionDownloadTask呼叫的物件方法。
    (2)如果使用cancel,就相當於同時將NSURLSessionDwonloadTask任務也被取消了。所以如果要重新下載就需要重新建立NSURLSessionDownloadTask物件,而且,下載的內容不是重頭開始
(3)如果使用cancel是無法恢復下載,但是為了能夠恢復下載就只能用 cancelByProducingResumeData:^(NSData * _Nullable resumeData)方法,其中這個方法中的resumeData儲存的是之前已經下載好的資料相關的資訊:檔名,儲存位置,已經下載好的資料的長度等資訊,並不是下載的資料本身。恢復下載也是需要通過這個resumeData來恢復,然後繼續下載。同時也要重新建立下載任務物件NSURLSessionDownloadTask。

 - (void)cancelByProducingResumeData:(void (^)(NSData *resumeData))completionHandler;

取消操作以後會呼叫一個Block,並傳入一個resumeData,該引數包含了繼續下載檔案的位置資訊。也就是說,當你下載了10M得檔案資料,暫停了。那麼你下次繼續下載的時候是從第10M這個位置開始的,而不是從檔案最開始的位置開始下載。因而為了儲存這些資訊,所以才定義了這個NSData型別的這個屬性:resumeData。這個data只包含了url跟已經下載了多少資料,不會很大,不用擔心記憶體問題。另外,session還提供了通過resumeData來建立任務的方法

   - (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData;

我們只需要在取消操作的回撥中記錄好resumeData,然後在恢復下載的適合通過上面的方法建立任務就好了,相比NSURLconnection簡單太多了。

關於檔案下載與暫停的分析
當使用NSURLSessionDownloadTask進行下載的時候,系統會在cache資料夾下建立一個下載的路徑,路徑下會有一個以”CFNetworking”打頭的.tmp檔案(以下簡稱”下載檔案”防止混淆),這個就是我們正在下載中的檔案。而當我們呼叫了cancelByProducingResumeData:方法後,會得到一個data檔案,通過String格式化後,發現是一個XML檔案,裡面包含了關於.tmp檔案的一些關鍵點的描述,包括”Range”,”key”,”下載檔案的路徑”等等.而原本存在於download檔案下的下載檔案,則被移動到了系統tmp資料夾目錄下.而當我們再次進行resume操作的時候,下載檔案則又被移回到了download資料夾下。

關於程式被殺掉的斷點續傳resumeData
根據上面的分析,基本可以得到以下結論:
1.DownloadTask每次進行斷點續傳的時候,會根據data檔案中的”路徑Key”去尋找下載檔案,然後校驗後再根據”Range”屬性去進行斷點續傳。
2.download資料夾中存放的只會是下載中的檔案,一旦暫停就會被移動到tmp資料夾下。
3.每個暫停得到的data檔案,與下載檔案一一對應。
3.斷點續傳只與tmp資料夾中的檔案有關。
  使用NSURLSessionDataTask可以很輕鬆實現斷點續傳,可是有個致命的缺點就是無法進行後臺下載,一點應用程式進入了後臺,便會停止下載。所以無法滿足我們的需求。而NSURLSessionDownloadTask是唯一可以實現後臺下載的類,所以我們只能從這個類進行下手了。

四、進行後臺下載任務
NSURLSession最大的優勢在於其後臺下載的靈活性,使用如下的程式碼進行後臺資料下載:

 NSURLSessionConfiguration * backgroundConfiguration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.zyprosoft.backgroundsession"];
    NSURLRequest * request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
    NSURLSession *  backgroundSession   = [NSURLSession sessionWithConfiguration:backgroundConfiguration delegate:self delegateQueue:nil];
    [[backgroundSession downloadTaskWithRequest:request]resume];

在下面的回撥方法中可以進行下載進度的監聽:

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
    NSLog(@"######");
}

如果在下載過程中點選Home鍵使應用程式進入後臺,NSURLSession的相關代理方法將不再被回撥,但是下載任務依然在進行,當後臺下載完成後會與AppDelegate進行互動,會呼叫AppDelegate中的如下方法:

-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler{
    NSLog(@"1111");
}

之後應用程式在後臺會呼叫NSURLSesstion代理的如下方法來通知下載結果:

//此方法無論成功失敗都會呼叫
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{
    NSLog(@"完成:error%@",error);
}
//此方法只有下載成功才會呼叫 檔案放在location位置
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{

}

最後將呼叫NSURLSesstion的如下方法:

-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{

    NSLog(@"All tasks are finished");

}

程式碼:
https://github.com/onebutterflyW/HFDownLoad
https://github.com/onebutterflyW/NSURLSession:中包含三個工程
NSURLSession-master是NSURLSession的簡單使用,資料的上傳下載
WMNSURLSessionHelper是封裝的NSURLSession的幫助類,使用completionHandler一次性處理所有資料
SimpleBackgroundTransfer是apple官方後臺下載的例子