NSURLSessionDownloadTask的後臺下載與斷點續傳

 

對於後臺下載與斷點續傳相信你們確定不會陌生,那麼若是要同時實現這兩種需求該怎麼辦呢?git

使用NSURLSessionDataTask能夠很輕鬆實現斷點續傳,但是有個致命的缺點就是沒法進行後臺下載,一點應用程序進入了後臺,便會中止下載.因此沒法知足咱們的需求.而NSURLSessionDownloadTask是惟一能夠實現後臺下載的類,因此咱們只能從這個類進行下手了github

網上關於NSURLSessionDownloadTask的斷點續傳資料不少,可是很遺憾的是基本都是如出一轍的CV大法.並且只有一個暫停按鈕暫停後繼續下載,而關於應用程序被關閉後的斷點續傳隻字不提.
那麼本篇咱們就來談談關於應用程序隨時可能被殺死的狀況下,如何進行斷點續傳.數據庫

關於斷點續傳原理:

首先,若是想要進行斷點續傳,那麼須要簡單瞭解一下斷點續傳的工做機制,在HTTP請求頭中,有一個Range的關鍵字,經過這個關鍵字能夠告訴服務器返回哪些數據給我
好比:
bytes=500-999 表示第500-第999字節
bytes=500- 表示從第500字節日後的全部字節
而後咱們再根據服務器返回的數據,將獲得的data數據拼接到文件後面,就能夠實現斷點續傳了.緩存

關於NSURLSessionDownloadTask基礎

你們能夠參考下這篇文章http://my.oschina.net/iOSliuhui/blog/469276安全

關於文件下載與暫停的分析

當使用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文件夾中的文件有關.session

具體實現

爲了節省性能,我嘗試查找關於程序被殺掉前的回調,可是很遺憾失敗了,由於我沒法控制到知道是哪一秒去保存進度,因此我只能每隔一段時間保存一次.設置一個變量已經下載的字節數,根據字節大小判斷,好比每下載1M保存一次(聽上去挺笨的,可是這彷佛是惟一得到data文件的辦法了).而後保存data文件和拷貝tmp文件夾下的下載文件到安全目錄下(由於tmp文件夾聽說隨時可能清空).
當再次下載的時候,先是從安全目錄下取到下載文件,刪除tmp文件夾中原有的同名文件,而後copy到tmp目錄下,最後利用保存的data文件進行再次downloadTaskWithResumeData操做,就能夠實現再次下載了.app

利與弊

好處:
1.DownloadTask能夠後臺下載,沒必要保持app在前臺,用戶體驗很好
2.實現了任意時間點殺掉進程後,仍然能夠斷點續傳ide

缺陷:
1.由於蘋果沒有提供很好的API,因此會有一個循環檢查,每隔一段時間會暫停個一秒左右,效率略有下降
2.若是設置保存間隔過長,中間殺掉進程可能會損失較多進度.性能

最後附上Demo的Github地址:
https://github.com/WeiTChen/NSURLSessionDownload.git

但願你們共同進步,若是這篇文章幫助到你,不妨點個贊鼓勵下吧!

//
//  ViewController.m
//  URLSession
//
//  Created by William on 16/4/26.
//  Copyright © 2016年 William. All rights reserved.
//

#import "ViewController.h"
#import <MediaPlayer/MediaPlayer.h>

#define _1M 1024*1024


@interface ViewController ()<NSURLSessionDownloadDelegate>
@property (nonatomic,strong) MPMoviePlayerController *moviePlayer;
@property (nonatomic,strong) NSURLSession *backgroundURLSession;

@property (nonatomic,strong) NSFileManager *manage;

@property (nonatomic,strong) NSString *docPath;

@property (nonatomic,strong) NSURLSessionDownloadTask *task;

@property (nonatomic,strong) NSData *fileData;

@property (nonatomic,strong) UILabel *lab;

@property (nonatomic,assign) long long int byte;

@end

@implementation ViewController
{
    NSString *dataPath;
    NSString *tmpPath;
    NSString *docFilePath;
}

-(MPMoviePlayerController *)moviePlayer{
    if (!_moviePlayer) {
        NSString *docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
        NSString *filePath = [docPath stringByAppendingPathComponent:@"file.mp4"];
        NSURL *url=[NSURL fileURLWithPath:filePath];
        _moviePlayer=[[MPMoviePlayerController alloc]initWithContentURL:url];
        _moviePlayer.view.frame=CGRectMake(0, 0, self.view.frame.size.width, 200);
        _moviePlayer.view.autoresizingMask=UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight;
        
    }
    return _moviePlayer;
}
- (NSString *)docPath
{
    if (!_docPath)
    {
        _docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
    }
    return _docPath;
}

- (NSFileManager *)manage
{
    if (!_manage)
    {
        _manage = [NSFileManager defaultManager];
    }
    return _manage;
}

- (NSURLSession *)backgroundURLSession
{
    static NSURLSession *session = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSString *identifier = @"background";
        NSURLSessionConfiguration* sessionConfig = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:identifier];
        session = [NSURLSession sessionWithConfiguration:sessionConfig
                                                              delegate:self
                                                         delegateQueue:[NSOperationQueue mainQueue]];
    });
    return session;
    
}

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
    
    NSString *filePath = [self.docPath stringByAppendingPathComponent:@"file.mp4"];
    [self.manage moveItemAtURL:location toURL:[NSURL fileURLWithPath:filePath] error:nil];
    [self.manage removeItemAtPath:dataPath error:nil];
    [self.manage removeItemAtPath:docFilePath error:nil];
    _fileData = nil;
    NSLog(@"下載完成%@",filePath);
}

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
    _lab.text = [NSString stringWithFormat:@"下載中,進度爲%.2f",totalBytesWritten*100.0/totalBytesExpectedToWrite];
    _byte+=bytesWritten;
    //1k = 1024字節,1M = 1024k,我這裏定義的每下載1M保存一次,你們能夠自行設置

    if (_byte > _1M)
    {
        [self downloadPause];
        _byte -= _1M;
    }
}

- (void)viewWillAppear:(BOOL)animated
{
//    [self.moviePlayer play];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view addSubview:self.moviePlayer.view];
    
    UIButton *start = [[UIButton alloc]initWithFrame:CGRectMake(60, 250, 40, 40)];
    [start setTitle:@"下載" forState:UIControlStateNormal];
    [start addTarget:self action:@selector(download) forControlEvents:UIControlEventTouchUpInside];
    start.backgroundColor = [UIColor orangeColor];
    [self.view addSubview:start];
    
    UIButton *pause = [[UIButton alloc]initWithFrame:CGRectMake(160, 250, 40, 40)];
    [pause setTitle:@"暫停" forState:UIControlStateNormal];
    [pause addTarget:self action:@selector(pause) forControlEvents:UIControlEventTouchUpInside];
    pause.backgroundColor = [UIColor orangeColor];
    [self.view addSubview:pause];
    
    dataPath = [self.docPath stringByAppendingPathComponent:@"file.db"];
    
    _lab = [[UILabel alloc]initWithFrame:CGRectMake(40, 300, 200, 40)];
    _lab.backgroundColor = [UIColor orangeColor];
    [self.view addSubview:_lab];
}

- (void)pause
{
    
    [_task cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
        _fileData = resumeData;
        _task = nil;
        [resumeData writeToFile:dataPath atomically:YES];
        [self getDownloadFile];
    }];
}

- (void)download
{
    NSString *downloadURLString = @"http://221.226.80.142:8082/Myftp/jlsj/file/ssqr/fckUplodFiles/201603/201603231521474489.mp4";
    NSURL* downloadURL = [NSURL URLWithString:downloadURLString];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:downloadURL];
    
    _fileData = [NSData dataWithContentsOfFile:dataPath];
    
    if (_fileData)
    {
        NSString *Caches = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;
        [self.manage removeItemAtPath:Caches error:nil];
        [self MoveDownloadFile];
        _task = [self.backgroundURLSession downloadTaskWithResumeData:_fileData];
        
    }
    else
    {
        _task = [self.backgroundURLSession downloadTaskWithRequest:request];
    }
    
    _task.taskDescription = [NSString stringWithFormat:@"後臺下載"];
    //執行resume保證開始了任務
    [_task resume];
    
    
}


//暫停下載,獲取文件指針和緩存文件
- (void)downloadPause
{
    
    NSLog(@"%s",__func__);
    [_task cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
        _fileData = resumeData;
        _task = nil;
        [resumeData writeToFile:dataPath atomically:YES];
        [self getDownloadFile];
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            //作完保存操做以後讓他繼續下載
            if (_fileData)
            {
                _task = [self.backgroundURLSession downloadTaskWithResumeData:_fileData];
                [_task resume];
            }
        });
    }];
}

//獲取系統生成的文件
- (void)getDownloadFile
{
    //調用暫停方法後,下載的文件會從下載文件夾移動到tmp文件夾
    NSArray *paths = [self.manage subpathsAtPath:NSTemporaryDirectory()];
    NSLog(@"%@",paths);
    for (NSString *filePath in paths)
    {
        if ([filePath rangeOfString:@"CFNetworkDownload"].length>0)
        {
            tmpPath = [self.docPath stringByAppendingPathComponent:filePath];
            NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:filePath];
            //tmp中的文件隨時有可能給刪除,移動到安全目錄下防止被刪除
            [self.manage copyItemAtPath:path toPath:tmpPath error:nil];
            
            //建議建立一個plist表來管理,能夠經過task的response的***name獲取到文件名稱,kvc存儲或者直接創建數據庫來進行文件管理,否則文件多了可能會管理混亂;
        }
    }
}

//講道理這個和上面的應該封裝下
- (void)MoveDownloadFile
{
    NSArray *paths = [self.manage subpathsAtPath:_docPath];
    
    for (NSString *filePath in paths)
    {
        if ([filePath rangeOfString:@"CFNetworkDownload"].length>0)
        {
            docFilePath = [_docPath stringByAppendingPathComponent:filePath];
            NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:filePath];
            //反向移動
            [self.manage copyItemAtPath:docFilePath toPath:path error:nil];
            
            //建議建立一個plist表來管理,能夠經過task的response的***name獲取到文件名稱,kvc存儲或者直接創建數據庫來進行文件管理,否則文件多了可能會管理混亂;
        }
    }
    NSLog(@"%@,%@",paths,[self.manage subpathsAtPath:NSTemporaryDirectory()]);
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    NSLog(@"%s", __func__);

}


@end
相關文章
相關標籤/搜索