NSURLSessionDownloadTask的后台下载与断点续传

跟風遠走 提交于 2019-12-07 12:07:42

 

对于后台下载与断点续传相信大家肯定不会陌生,那么如果要同时实现这两种需求该怎么办呢?

使用NSURLSessionDataTask可以很轻松实现断点续传,可是有个致命的缺点就是无法进行后台下载,一点应用程序进入了后台,便会停止下载.所以无法满足我们的需求.而NSURLSessionDownloadTask是唯一可以实现后台下载的类,所以我们只能从这个类进行下手了

网上关于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文件夹中的文件有关.

具体实现

为了节省性能,我尝试查找关于程序被杀掉前的回调,但是很遗憾失败了,因为我无法控制到知道是哪一秒去保存进度,所以我只能每隔一段时间保存一次.设置一个变量已经下载的字节数,根据字节大小判断,比如每下载1M保存一次(听上去挺笨的,但是这似乎是唯一获得data文件的办法了).然后保存data文件和拷贝tmp文件夹下的下载文件到安全目录下(因为tmp文件夹据说随时可能清空).
当再次下载的时候,先是从安全目录下取到下载文件,删除tmp文件夹中原有的同名文件,然后copy到tmp目录下,最后利用保存的data文件进行再次downloadTaskWithResumeData操作,就可以实现再次下载了.

利与弊

好处:
1.DownloadTask可以后台下载,不必保持app在前台,用户体验很好
2.实现了任意时间点杀掉进程后,仍然可以断点续传

缺陷:
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

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!