iOS使用NSURLSession進(jìn)行下載(包括后臺(tái)下載,斷點(diǎn)下載)

從iOS7以來,蘋果推出NSURLSession后,iOS現(xiàn)在可以實(shí)現(xiàn)真正的后臺(tái)下載,這對(duì)我們iOSer來說是一個(gè)福音。

一個(gè) NSURLSession對(duì)象可以協(xié)調(diào)一個(gè)或多個(gè) NSURLSessionTask對(duì)象,并根據(jù)NSURLSessionTask 創(chuàng)建的 NSURLSessionConfiguration 實(shí)現(xiàn)不同的功能。使用相同的配置,你也可以創(chuàng)建多組具有相關(guān)任務(wù)的 NSURLSession 對(duì)象。要利用后臺(tái)傳輸服務(wù),你將會(huì)使用 [NSURLSessionConfiguration backgroundSessionConfiguration]來創(chuàng)建一個(gè)會(huì)話配置。添加到后臺(tái)會(huì)話的任務(wù)在外部進(jìn)程運(yùn)行,即使應(yīng)用程序被掛起,崩潰,或者被殺死,它依然會(huì)運(yùn)行。

下面我們來看看如何使用NSURLSession

下載用到的委托方法

  1. AppDelegate委托方法
//在應(yīng)用處于后臺(tái),且后臺(tái)任務(wù)下載完成時(shí)回調(diào)
 - (void)application:(UIApplication *)application
handleEventsForBackgroundURLSession:(NSString *)identifier 
 completionHandler:(void (^)())completionHandler;
  1. NSURLSession委托方法
/* 在任務(wù)下載完成、下載失敗
  * 或者是應(yīng)用被殺掉后,重新啟動(dòng)應(yīng)用并創(chuàng)建相關(guān)identifier的Session時(shí)調(diào)用
  */
 - (void)URLSession:(NSURLSession *)session
                 task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error;
/* 應(yīng)用在后臺(tái),而且后臺(tái)所有下載任務(wù)完成后,
 * 在所有其他NSURLSession和NSURLSessionDownloadTask委托方法執(zhí)行完后回調(diào),
 * 可以在該方法中做下載數(shù)據(jù)管理和UI刷新
  */
 - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session;

注:最好將handleEventsForBackgroundURLSessioncompletionHandler保存,在該方法中待所有載數(shù)據(jù)管理和UI刷新做完后,再調(diào)用completionHandler()

  1. NSURLSessionDownloadTask委托方法
/* 下載過程中調(diào)用,用于跟蹤下載進(jìn)度
 * bytesWritten為單次下載大小
 * totalBytesWritten為當(dāng)當(dāng)前一共下載大小
 * totalBytesExpectedToWrite為文件大小
 */
 - (void)URLSession:(NSURLSession *)session
      downloadTask:(NSURLSessionDownloadTask *)downloadTask
      didWriteData:(int64_t)bytesWritten
 totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite;
/* 下載恢復(fù)時(shí)調(diào)用
 * 在使用downloadTaskWithResumeData:方法獲取到對(duì)應(yīng)NSURLSessionDownloadTask,
 * 并該task調(diào)用resume的時(shí)候調(diào)用
  */
 - (void)URLSession:(NSURLSession *)session
      downloadTask:(NSURLSessionDownloadTask *)downloadTask
 didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes;
//下載完成時(shí)調(diào)用
 - (void)URLSession:(NSURLSession *)session
      downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location;

注:在URLSession:downloadTask:didFinishDownloadingToURL方法中,location只是一個(gè)磁盤上該文件的臨時(shí) URL,只是一個(gè)臨時(shí)文件,需要自己使用NSFileManager將文件寫到應(yīng)用的目錄下(一般來說這種可以重復(fù)獲得的內(nèi)容應(yīng)該放到cache目錄下),因?yàn)楫?dāng)你從這個(gè)委托方法返回時(shí),該文件將從臨時(shí)存儲(chǔ)中刪除。

創(chuàng)建后臺(tái)下載的操作步驟

后臺(tái)傳輸?shù)牡膶?shí)現(xiàn)也十分簡(jiǎn)單,簡(jiǎn)單說分為三個(gè)步驟:

  1. 創(chuàng)建后臺(tái)下載用的NSURLSession對(duì)象,設(shè)置為后臺(tái)下載類型;
  2. 向這個(gè)對(duì)象中加入對(duì)應(yīng)的傳輸?shù)腘SURLSessionTask,并開始下載;
  3. 在AppDelegate里實(shí)現(xiàn)handleEventsForBackgroundURLSession,以刷新UI及通知系統(tǒng)傳輸結(jié)束。
  4. 實(shí)現(xiàn)NSURLSessionDownloadDelegate中必要的代理

下面用代碼來說明描述后臺(tái)下載的流程

首先,我們看下后臺(tái)下載的時(shí)序圖

后臺(tái)下載時(shí)序圖

具體代碼實(shí)現(xiàn)

  1. 創(chuàng)建一個(gè)后臺(tái)下載對(duì)象
    用dispatch_once創(chuàng)建一個(gè)用于后臺(tái)下載對(duì)象,目的是為了保證identifier的唯一,文檔不建議對(duì)于相同的標(biāo)識(shí)符 (identifier) 創(chuàng)建多個(gè)會(huì)話對(duì)象。這里創(chuàng)建并配置了NSURLSession,將通過backgroundSessionConfiguration其指定為后臺(tái)session并設(shè)定delegate。
 - (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;
}
  1. 向其中加入對(duì)應(yīng)的傳輸用的NSURLSessionTask,并調(diào)用resume啟動(dòng)下載。
 - (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];
}
  1. 在appDelegate中實(shí)現(xiàn)handleEventsForBackgroundURLSession,要注意的是,需要在handleEventsForBackgroundURLSession中必須重新建立一個(gè)后臺(tái) session 的參照(可以用之前dispatch_once創(chuàng)建的對(duì)象),否則 NSURLSessionDownloadDelegateNSURLSessionDelegate 方法會(huì)因?yàn)闆]有 對(duì) session 的 delegate 設(shè)置而不會(huì)被調(diào)用。
    然后保存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];    
}
 - (void)addCompletionHandler:(CompletionHandlerType)handler 
  forSession:(NSString *)identifier {
        if ([self.completionHandlerDictionary objectForKey:identifier]) {
           NSLog(@"Error: Got multiple handlers for a single session identifier.  This should not happen.\n");
        }
        [self.completionHandlerDictionary setObject:handler forKey:identifier];
}

注:handleEventsForBackgroundURLSession方法是在后臺(tái)下載的所有任務(wù)完成后才會(huì)調(diào)用。如果當(dāng)后臺(tái)傳輸完成時(shí),如果應(yīng)用程序已經(jīng)被殺掉,iOS將會(huì)在后臺(tái)啟動(dòng)該應(yīng)用程序,下載相關(guān)的委托方法會(huì)在 application:didFinishLaunchingWithOptions:方法被調(diào)用之后被調(diào)用。

  1. 實(shí)現(xiàn)URLSessionDidFinishEventsForBackgroundURLSession,待所有數(shù)據(jù)處理完成,UI刷新之后在改方法中在調(diào)用之前保存的completionHandler()。
//NSURLSessionDelegate委托方法,會(huì)在NSURLSessionDownloadDelegate委托方法后執(zhí)行
 - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
        NSLog(@"Background URL session %@ finished events.\n", session);
        if (session.configuration.identifier) {
            // 調(diào)用在 -application:handleEventsForBackgroundURLSession: 中保存的 handler
            [self callCompletionHandlerForSession:session.configuration.identifier];
        }
}
 - (void)callCompletionHandlerForSession:(NSString *)identifier {
        CompletionHandlerType handler = [self.completionHandlerDictionary objectForKey: identifier];
        if (handler) {
           [self.completionHandlerDictionary removeObjectForKey: identifier];
           NSLog(@"Calling completion handler for session %@", identifier);
            handler();
        }
}
  1. 在此,后臺(tái)下載的基本功能已經(jīng)具備了,如果還需要監(jiān)聽下載進(jìn)度和對(duì)下載完成數(shù)據(jù)進(jìn)行處理,則需要實(shí)現(xiàn)上面提到的委托方法
    URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:URLSession:downloadTask:didFinishDownloadingToURL:

關(guān)于斷點(diǎn)下載

對(duì)于斷點(diǎn)下載需要考慮幾個(gè)問題:

  1. 如何暫停下載,暫停后,如何繼續(xù)下載?
  • 下載失敗后,如何恢復(fù)下載?
  • 應(yīng)用被用戶殺掉后,如何恢復(fù)之前的下載?

針對(duì)這幾個(gè)問題,我們一個(gè)來分析

  • 如何暫停下載,暫停后,如何繼續(xù)下載?
    有兩種方法
  • 第一種,使用cancelByProducingResumeData
    /* 對(duì)某一個(gè)NSURLSessionDownloadTask取消下載,取消后會(huì)回調(diào)給我們 resumeData,
    * resumeData包含了下載任務(wù)的一些狀態(tài),之后可以用戶恢復(fù)下載
   */
   - (void)cancelByProducingResumeData:(void (^)(NSData * resumeData))completionHandler;

調(diào)用該方法會(huì)觸發(fā)以下方法,會(huì)附帶resumeData,用于恢復(fù)。

    - (void)URLSession:(NSURLSession *)session
              task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error 

對(duì)應(yīng)恢復(fù)方法

//通過之前保存的resumeData,獲取斷點(diǎn)的NSURLSessionTask,調(diào)用resume恢復(fù)下載
NSURLSessionDownloadTask *task = [[self backgroundURLSession] downloadTaskWithResumeData:resumeData];
[task resume];
  • 第二種,使用NSURLSessionDownloadTask的suspend方法
//暫停
[self.downloadTask suspend];
//恢復(fù)
[self.downloadTask resume];

通過以上的兩個(gè)方法,就可以實(shí)現(xiàn)下載的暫停與恢復(fù)下載了

  • 下載失敗后,如何恢復(fù)下載?
    下載失敗后,可以通過以下代碼來恢復(fù)下載
  /* 該方法下載成功和失敗都會(huì)回調(diào),只是失敗的是error是有值的,
  * 在下載失敗時(shí),error的userinfo屬性可以通過NSURLSessionDownloadTaskResumeData
  * 這個(gè)key來取到resumeData(和上面的resumeData是一樣的),再通過resumeData恢復(fù)下載
  */
 - (void)URLSession:(NSURLSession *)sessiona
              task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
    if (error) {
        // check if resume data are available
        if ([error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData]) {
            NSData *resumeData = [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData];
            NSURLSessionTask *task = [[self backgroundURLSession] downloadTaskWithResumeData:resumeData];
            [task resume];
        }
    }
}
  • 應(yīng)用被用戶殺掉后,如何恢復(fù)之前的下載?
    在應(yīng)用被殺掉前,iOS系統(tǒng)保存應(yīng)用下載sesson的信息,在重新啟動(dòng)應(yīng)用,并且創(chuàng)建和之前相同identifier的session時(shí)(蘋果通過identifier找到對(duì)應(yīng)的session數(shù)據(jù)),iOS系統(tǒng)會(huì)對(duì)之前下載中的任務(wù)進(jìn)行依次回調(diào)URLSession:task:didCompleteWithError:方法,之后可以使用上面提到的下載失敗時(shí)的處理方法進(jìn)行恢復(fù)下載

知道這些后,看下前臺(tái)下載的時(shí)序圖對(duì)整個(gè)下載流程就了解了。


前臺(tái)下載時(shí)序圖.png

關(guān)于Session的生命周期,可以閱讀 Apple 的 Life Cycle of a URL Session with Custom Delegates 文檔,它講解了所有類型的會(huì)話任務(wù)的完整生命周期。

后臺(tái)下載的配置和限制

NSURLSessionConfiguration 允許你設(shè)置默認(rèn)的HTTP頭,配置緩存策略,限制使用蜂窩數(shù)據(jù)等等。其中一個(gè)選項(xiàng)是discretionary標(biāo)志,這個(gè)標(biāo)志允許系統(tǒng)為分配任務(wù)進(jìn)行性能優(yōu)化。這意味著只有當(dāng)設(shè)備有足夠電量時(shí),設(shè)備才通過 Wifi 進(jìn)行數(shù)據(jù)傳輸。如果電量低,或者只僅有一個(gè)蜂窩連接,傳輸任務(wù)是不會(huì)運(yùn)行的。后臺(tái)傳輸總是在 discretionary模式下運(yùn)行。timeoutIntervalForResource屬性,支持資源超時(shí)特性。你可以使用這個(gè)特性指定你允許完成一個(gè)傳輸所需的最長(zhǎng)時(shí)間。內(nèi)容只在有限的時(shí)間可用,或者在用戶只有有限Wifi帶寬的時(shí)間內(nèi)無法下載或上傳資源的情況下,你也可以使用這個(gè)特性。

最后,我們來說一說使用后臺(tái)會(huì)話的幾個(gè)限制。作為一個(gè)必須實(shí)現(xiàn)的委托,您不能對(duì)NSURLSession使用簡(jiǎn)單的基于 block的回調(diào)方法。后臺(tái)啟動(dòng)應(yīng)用程序,是相對(duì)耗費(fèi)較多資源的,所以總是采用HTTP重定向。后臺(tái)傳輸服務(wù)只支持HTTP和HTTPS,你不能使用自定義的協(xié)議。系統(tǒng)會(huì)根據(jù)可用的資源進(jìn)行優(yōu)化,在任何時(shí)候你都不能強(qiáng)制傳輸任務(wù)在后臺(tái)進(jìn)行。

另外,要注意的是在后臺(tái)會(huì)話中,NSURLSessionDataTasks 是完全不支持的,你應(yīng)該只出于短期的,小請(qǐng)求為目的使用這些任務(wù),而不是用來下載或上傳。其中發(fā)現(xiàn)一些需要注意的點(diǎn),記錄下來。

NSURLSession在iOS10上的Bug

在iOS10上,resumeData不能直接使用,會(huì)提示以下錯(cuò)誤

*** -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL
*** -[NSKeyedUnarchiver initForReadingWithData:]: data is NULL
Invalid resume data for background download. Background downloads must use http or https and must download to an accessible file.

解決方法有點(diǎn)小麻煩,具體解決方法可以參照以下Demo

關(guān)于在后臺(tái)啟動(dòng)新的下載任務(wù),蘋果對(duì)這方面有限制

大致說明如下:

1.蘋果的NSURLSession這個(gè)類會(huì)維護(hù)一個(gè)Delay值(即延時(shí)執(zhí)行時(shí)間),用于后臺(tái)啟動(dòng)任務(wù)延時(shí)執(zhí)行時(shí)使用;
2.當(dāng)在后臺(tái)啟動(dòng)一個(gè)新任務(wù)時(shí),蘋果會(huì)對(duì)這個(gè)任務(wù)進(jìn)行延時(shí)執(zhí)行,延時(shí)時(shí)間蘋果那邊是有一個(gè)默認(rèn)的延時(shí)時(shí)間,當(dāng)后臺(tái)啟動(dòng)的任務(wù)數(shù)越多,這個(gè)值就會(huì)成2的N-1冪倍增長(zhǎng);
3.比如:假設(shè)蘋果設(shè)定的延時(shí)時(shí)間為Delay。當(dāng)在后臺(tái)啟動(dòng)了第一個(gè)任務(wù)時(shí),這個(gè)任務(wù)的延時(shí)時(shí)間為Delay,這個(gè)任務(wù)會(huì)在Delay時(shí)間后開始執(zhí)行;當(dāng)啟動(dòng)在后臺(tái)啟動(dòng)第二個(gè)任務(wù)時(shí),這個(gè)任務(wù)的延時(shí)時(shí)間為2*Delay,當(dāng)啟動(dòng)第三個(gè)任務(wù)是,該任務(wù)的延時(shí)執(zhí)行時(shí)間即為2*2*Delay以此類推,在后臺(tái)啟動(dòng)第N個(gè)任務(wù)是,該任務(wù)的延時(shí)執(zhí)行時(shí)間為2(N-1)次方*Delay
4.但是在應(yīng)用從后臺(tái)切到前臺(tái)或者重新啟動(dòng)時(shí),這個(gè)延時(shí)時(shí)間會(huì)重置。

所以蘋果對(duì)在后臺(tái)啟動(dòng)新的下載或者上傳任務(wù)時(shí),是有限制的,蘋果也是不建議這么處理的

以下為蘋果官方說明的鏈接地址:
https://forums.developer.apple.com/thread/14854

簡(jiǎn)單的下載Demo

Demo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評(píng)論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,497評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評(píng)論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,727評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,193評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,411評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,945評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,777評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,978評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,216評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評(píng)論 1 286
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,657評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,960評(píng)論 2 373

推薦閱讀更多精彩內(nèi)容