以前使用NSURLSession作了一個斷點續傳的demo,主要實現了在下載的過程當中中斷下載,而後能夠再次啓動延續上次的下載連接繼續下載的功能.原理是將task的方法cancelByProducingResumeData的Block塊中的resumeData獲取下來,當再次下載的時候,經過session的downloadTaskWithResumeData方法使用該resumeData建立一個新的task,而後啓動下載,就實現了斷點續傳的功能.可是若是說當前任務正在下載,程序切到後臺以後被kill掉,當再次啓動應用的時候,就沒法繼續上次的下載,也就是說,剛纔的那種思路,只是適用於用戶手動暫停在程序不退出的狀況下實現的斷點續傳,若是應用直接終結則不會繼續下載,也就是說並非真正意義上的斷點續傳,由於再次啓動應用的時候,仍然要從新下載.git
因而我就在考慮,如何可以實現當應用意外退出的時候,再次啓動應用,仍然能夠繼續上次的下載任務.咱們知道,resumenData中保存的數據是當前任務的下載信息,將其反序列化出來成爲字符串的格式輸出的話,其內容以下所示(部分冗餘內容已經切除,重要信息已添加註釋):github
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>NSURLSessionDownloadURL</key> <string>http://oarbi0614.bkt.clouddn.com/%E5%86%B0%E6%B2%B3%E4%B8%96%E7%BA%AA.mp4</string> 請求的地址 <key>NSURLSessionResumeBytesReceived</key> <integer>12038723</integer> 當前下載的文件大小 <key>NSURLSessionResumeCurrentRequest</key> <data> YnBsaXN0MDD..... </data> <key>NSURLSessionResumeEntityTag</key> <string>"ljECR82nRMhHvP8D5M9sGQuKBjgK"</string> <key>NSURLSessionResumeInfoTempFileName</key> <string>CFNetworkDownload_6XdKfZ.tmp</string> 下載使用的臨時文件名 <key>NSURLSessionResumeInfoVersion</key> <integer>2</integer> <key>NSURLSessionResumeOriginalRequest</key> <data> YnBsaXN0MDD... </data> <key>NSURLSessionResumeServerDownloadDate</key> <string>Mon, 25 Jul 2016 10:42:23 GMT</string> </dict> </plist>
這個數據塊裏保存了當前下載的的狀態,包括臨時文件的名字以及當前下載的文件大小.當文件正在下載的時候,會在tmp文件夾內生成一個.tmp文件,保存了當前實際下載的數據,當經過session的downloadTaskWithResumeData方法使用該resumeData建立一個新的task,而後啓動下載的時候,task會經過該數據塊找到這個文件,而後繼續下載.這樣一來,貌似咱們只須要將resumeData數據塊保存下來而且保存存儲數據的.tmp文件就行了,當應用意外退出再次開啓下載任務的時候,咱們只須要使用resumeData建立下載任務,而後將.tmp文件放在tmp文件夾下就行了.數據庫
那麼如今問題到了如何在程序意外退出的時候如何保存resumeData數據塊和.tmp文件上了.咱們該如何實現呢?緩存
方法一:當程序意外退出的時候,當前的控制器會調用-(void)viewWillDisappear:(BOOL)animated,應用的代理會調用-applicationWillTerminate:(UIApplication *)application,咱們能夠在這兩個方法裏作文章.session
在-(void)viewWillDisappear:(BOOL)animated或者-applicationWillTerminate:(UIApplication *)application方法裏將resumeData保存到cache文件夾中
要取出resuemData,必然是要經過併發
__weak typeof(self)weakSelf = self; [self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) { //在這裏能夠獲取到resumeData weakSelf.resumeData = resumeData; }];
這個方法來獲取resumeData,可是在實際使用的時候,卻遇到了這樣的問題,不管是將該代碼放在-(void)viewWillDisappear:(BOOL)animated仍是-applicationWillTerminate:(UIApplication *)application方法裏,weakSelf.resumeData裏始終是空的,通過試驗,當程序運行的時候,該block塊裏的代碼是不走的,可是在該Block塊外的代碼,所有都是執行的.咱們都知道,當建立session的時候,若是設置queue的時候傳入參數爲nil,那麼他的代理方法都是在全局併發隊列裏完成的,難道是由於這個緣由麼?是否是當程序意外退出的時候,這兩個方法裏的子線程代碼都不會執行?我作了以下的實驗:app
在這兩個方法裏添加以下代碼
dispatch_async(dispatch_get_global_queue(0, 0), ^{ for(int i = 0; i < 100; i ++){ NSLog(@"%d",i); } });
當在後臺kill掉應用的時候,有時候輸出0 ,可是若是把下面的代碼添加到該方法裏,裏面的代碼仍然是不執行的async
dispatch_async(dispatch_get_main_queue(), ^{ for(int i = 0; i < 100; i ++){ NSLog(@"%d",i); } });
可是若是隻把性能
for(int i = 0; i < 100; i ++){ NSLog(@"%d",i); }
這幾行代碼放在這兩個方法中,則都是能夠完整的執行的,使人百思不得其解.測試
可是思考一下能夠發現:
- 當咱們設置session爲主線程的時候,通過檢查能夠發現cancelByProducingResumeData方法裏的Block塊是在主線程運行的;
- 當咱們設置session線程爲nil的時候,其代理方法是在全局併發隊列裏執行的,包括cancelByProducingResumeData方法裏的Block塊也是在子線程中運行的.
因此咱們能夠推測一下,在OC底層,cancelByProducingResumeData方法頗有可能也是像dispatch_async方法同樣是經過向線程隊列裏添加任務來得到執行機會的.加上上面的三個實驗,我猜想是當程序終結的時候,只會執行主線程中的代碼,此時若是再經過獲取線程向主線程添加任務的話,那麼該任務就不會添加到主線程隊列裏去,更別提往子線程裏添加任務了.OC是編譯型語言,當應用終結的時候,只有那些寫到主線程上面的,編譯好的代碼在程序終結的時候能夠獲得執行,而在終結的時候動態添加的任務則沒法添加成功,因此在這兩個方法裏,當程序意外終結的時候,是不可以獲取到resumeData的.
方法二:動態保存
既然不能在程序終結的時候獲取到resumeData,那麼只能在下載的過程當中動態的保存了.當應用在下載的時候,會頻繁的調用其代理方法:
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{ } }
每當有數據過來的時候,都會調用這個方法,通過試驗能夠發現這個方法的調用頻率是很高的,尤爲是在全局併發隊列裏執行的時候,根據測試,該方法的調用頻率達到了1200次以上,因此若是每次下載都進行保存的話,那麼將會大大的影響應用的效率,由於文件流的輸入輸出自己就很佔用性能,再加上如此高頻率的調用,對性能的影響是不敢想像的.因此我作了這樣的設計,每當下載文件的十分之一時,獲取resumeData.
那麼獲取resumeData的問題解決了,接下來就是獲取.tmp文件了.若是咱們要對.tmp文件進行操做,那麼就必需要獲取到該文件完整的文件名,由於tmp文件夾下絕大多數文件都是.tmp結尾的,並且每次下載產生的.tmp文件都是不一樣的,因此若是將全部.tmp文件都保存的話,確定是很不合理的.這時候,就用到了咱們獲取到的resumeData,由於通過反序列化咱們能夠知道,resumeData中是保存着當前下載的臨時文件名的,因此咱們能夠對resumeData解析以後,取出其中的臨時文件名,並且當下載的時候,其確定是放在tmp文件夾下的,有了這些東西,咱們就能夠對.tmp文件進行保存了.
最終採用的方法流程:
由方法二咱們能夠獲取到resumeData和.tmp文件,有了這兩個文件,咱們就能夠在應用下次啓動的時候繼續上次未完成的下載,在文件每下載十分之一的時候保存一次.具體操做以下:
在session下載代理方法裏檢測文件下載過程,每當下載超過十分之一的時候,獲取resumeData數據塊和.tmp文件的路徑,而後將resumeData寫入到Cache文件夾下,將.tmp文件拷貝至Cache文件夾下,同時在Cache文件夾下創建一個plist文件,key值爲.tmp文件的文件名,value值是一個Bool值,標記該文件是否下載完成,至關於該plist文件是管理下載文件的目錄;
當應用再次啓動的時候,首先從Cache文件夾下讀取下載目錄,查找未下載完成的文件,找到後將以該文件命名的.tmp臨時文件複製到tmp文件夾中,而後取出」Resume_」 + 該文件名 命名的上次保存的resumeData數據,使用該數據建立task並開啓下載.
備註:Caches文件夾是蘋果爲用戶提供的緩存路徑,應用重啓該目錄不會清考,tmp文件夾會清空,而Documents目錄下的文件備份的時候被上傳到iCloud而且很快就用完有限的空間,因此咱們選擇在cache文件夾下緩存咱們須要的文件.
通過這樣的處理,基本就實現了當程序意外退出的時候,再次啓動仍然能夠繼續上次未完成的下載.不過對性能有些許影響並且不會百分之百續傳上次未下載完的數據,會丟失一點點(由於並非即時保存的).若是使用FMDB或者其餘的數據庫來管理緩存的文件的話,效果會更好些,不過此次想本身動手寫這些東西,就沒有用.
這個功能我寫了一個Demo,當下載好再次啓動的時候,會直接播放本地文件,沒有下載完會繼續下載,若沒有緩存數據則會從新下載.Demo裏包括了我本身封裝的一個基本的播放器和進度指示器.緩存的功能我寫成了一個單例放在了工程裏,方便給各位看官查看.這個Demo放在了GitHub上,地址爲https://github.com/TheRuningAnt/KillAppDownload.git,歡迎下載查看.
若是各位有更好的思路或者建議的話,跪求指點,多謝!