离线断点下载

使用NSURLSessionDataTask实现大文件离线断点下载(完整)

(1)实现思路

1. 大文件下载内存问题:边接收数据边写文件以解决内存越来越大的问题。注意文件句柄或输出流的关闭与清空。
2. 断点下载实现:文件下载默认是从头下载。可通过设置可变request的资源下载范围即可实现断点下载。注意进度计算与显示的细节处理。
3. 离线下载实现:每次下载任务时将文件总大小写入到沙盒中,下次从沙盒中读取;通过fileManager获取已下载文件大小继续下载。根据已下载大小和总大小计算初始进度并显示

(2)实现步骤:

1. viewDidLoad中,获取总文件大小,对总文件大小进行非空判断,若空,则不需要设置下载进度;否则计算并显示下载进度
2. 懒加载dataTask,内部设置请求体,设置资源下载范围,向服务器发送请求;每次取消,都要重新生成dataTask,发送网络请求。
3. 懒加载回话对象,内部根据configuration生成回话对象并设置代理
4. 懒加载已下载文件大小,内部通过文件管理者获取沙盒中已下载文件大小
5. 服务器响应时:
    1. 判断是否已经存在文件总大小,如果不存在,获取文件总大小,并写入到沙盒中
    2. 通过响应头获取服务器建议的文件名并写入到沙盒中(或直接用宏定义记录文件名)
    3. 获取caches文件夹路径,根据建议的文件名或宏拼接全路径
    4. 创建输出流并开启,记录成员变量数据(也可以通过获取文件管理者,创建文件用于保存数据),注意非空判断,如果已存在输出流,直接跳过;
    5. 实现blcok,允许接收数据

6 服务器返回数据时(多次调用):
    1. 计算进度并显示
        1 根据data.length计算出已下载数据量
        2 设置进度条的进度self.progressView.progress = 1.0* self.currentLength/self.totalLength
    2. 写入文件:
        1 让输出流写入数据。(或者通过创建文件句柄,让文件句柄写入数据,它每次写入数据,都会自动接着上次的地方继续写入)(边接受边写入的核心)。
        2 不能简单的通过data writeToFile,这样每次都会覆盖之前的数据)

7 请求结束后:
    1. 关闭输出流
    2. 清空输出流对象

8 提供暂停下载、取消下载,继续功能。
    1. 暂停下载使用系统的暂停功能
    2. 取消下载,清空dataTask与currentSize。
    3. 继续下载,重新resume。

9 屏幕跳转或屏幕即将消失时,需要使回话对象不可用并清空回话成员属性

(2)实现细节:

1. viewDidLoad中,获取已下载文件大小及总文件大小,显示下载进度;要对总文件大小做非空判断,为空直接返回
2. 懒加载已下载文件大小,内部通过文件管理者获取文件的属性--大小。文件名可以自取,也可以以服务器建议的名字保存,若通过这种方式,在保存总文件大小时也要保存建议的文字(字典要进行非空判断)
3. 总文件大小获取:内部通过读取沙盒中保存的文件大小获取数据,读取字典数据时,要对字典进行非空判断,若为空,则表示文件未创建;
4. 总文件大小保存:在首次发送网络请求并接收数据时写入磁盘,也需要对字典进行非空判断,字典为空,则表示文件未创建,需要创建可变字典再添加数据
5. 懒加载回话对象,这样就不会由于每次要创建任务而不断创建回话对象
6. 懒加载下载任务,内部设置可变请求对象,请求下载范围由已下载文件大小决定;要对进度进行非空判断,如果为1,即下载完成,就没必要再新建下载任务,直接return nil
7. 服务器响应时,需要对总文件大小做非空判断,如果不为空,不用再次记录文件总大小并保存;对输出流进行非空判断,为空才创建
8. 接收数据时,已下载文件大小需要不断与data.length求和计算;输出流要写入数据
9. 请求结束时,需要关闭输出流,清空输出流;
10. 需在屏幕跳转或者屏幕消失时关闭回话对象并清空回话成员变量,否则会与控制器循环引用,造成内存泄露。
11. 取消任务:需清空dataTask与currentSize,重新根据沙盒中已下载文件大小发送网络请求。否则取消后再继续下载的视频质量非常差
#import "ViewController.h"
#define KfileName @"minion_02.mp4"
#define KtotalSizeFileName @"fzq.fzq"
#define BASEURL (@"http://120.25.226.186:32812/resources/videos/minion_02.mp4")


@interface ViewController ()
/** 输出流 */
@property (nonatomic ,strong)NSOutputStream *stream;
/** 文件的总大小*/
@property(nonatomic ,assign)NSInteger totalSize;
/** 已经下载的文件的大小*/
@property(nonatomic ,assign)NSInteger currentSize;
/** session回话对象 */
@property (nonatomic ,strong)NSURLSession *session;
/** dataTask 任务*/
@property (nonatomic ,strong)NSURLSessionDataTask *dataTask;
/** 进度条 */
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;
@end

@implementation ViewController

-(void)viewDidLoad
{
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor blackColor];

    //获取文件的总大小
    NSInteger totalLength = [self getTotalSize];

    //计算已经下载了多少,也就是文件的下载进度
    if (totalLength == 0) {
        NSLog(@"说明第一次下载");
    }else
    {
        self.progressView.progress = 1.0 *self.currentSize/totalLength;
    }

}
#pragma mark -----------------
#pragma mark lazy loading
-(NSInteger)currentSize
{
    if (!_currentSize) {

        //    1.拼接文件的全路径
        NSString *fullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:KfileName];

        //可以拿到文件
        NSFileManager *manager = [NSFileManager defaultManager];
        NSDictionary *fileDict =[manager attributesOfItemAtPath:fullPath error:nil];

        _currentSize = [fileDict[@"NSFileSize"] integerValue];
    }

    return _currentSize;
}

-(NSURLSession *)session
{
    if (_session == nil) {
        //1.创建session
        /*
         第一个参数:配置信息
         第二个参数:代理 谁成为代理
         第三个参数:队列 决定代理方法在哪个线程
         */
        _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    }
    return _session;
}

-(NSURLSessionDataTask *)dataTask
{

    if (_dataTask == nil) {

        //判断当前文件是否已经下载完成了
        if (self.progressView.progress == 1.0) {
            NSLog(@"文件已经下载完成了");
            return nil;
        }

        //2.创建task
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:BASEURL]];

        NSString *range = [NSString stringWithFormat:@"bytes=%zd-",self.currentSize];

        [request setValue:range forHTTPHeaderField:@"Range"];

        _dataTask = [self.session dataTaskWithRequest:request];


    }
    return _dataTask;
}

#pragma mark -----------------
#pragma mark Metheds
//保存文件的总大小到沙盒中
-(void)saveTotalSize
{
    //1.拼接文件的全路径(xmg.xmg文件存放在什么地方)
    NSString *totalSizefullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:KtotalSizeFileName];

    //2.把字典写入到沙盒中
    /*这里有两种情况
     可能字典已经存在:那么取出的字典有值,可以直接使用
     可能字典不存在:那么取出的字典为空,需要重新创建
     */
    //根据路径把字典取出来
    NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:totalSizefullPath];

    //对字典做非空判断,为空则创建可变字典
    if (dict == nil) dict = [NSMutableDictionary dictionary];

    //3.修改字典(添加键值对)
    [dict setObject:@(self.totalSize) forKey:KfileName];

    //4.需要把字典回写
    [dict writeToFile:totalSizefullPath atomically:YES];
}

//获得文件的总大小
-(NSInteger )getTotalSize
{
    //1.获取文件的全路径(xmg.xmg)
    NSString *totalSizefullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:KtotalSizeFileName];

    //2.根据全路径获得沙盒中的字典
    /*
     字典可能不存在(没有下载过):那么说明之前该文件没有被下载过,直接返回0
     字典存在:返回对应文件的文件总大小
     */
    NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:totalSizefullPath];

    //对字典进行判断,输出对应结果
    if (dict == nil) {
        return 0 ;
    }else
    {
        //输出文件记录的下载文件总大小
        return [dict[KfileName]integerValue];
    }
}
#pragma mark -----------------
#pragma mark Events
//开始下载
- (IBAction)startBtnClcik:(id)sender
{
    [self.dataTask resume];
}
//暂停下载
- (IBAction)suspendBtnClick:(id)sender
{
    [self.dataTask suspend];
}
//继续下载
- (IBAction)goOnBtnClick:(id)sender
{
    NSLog(@"%@",self.dataTask);
    [self.dataTask resume];
}

//取消下载
- (IBAction)cancel {
    [self.dataTask cancel];

    //需清空下列数据,重新根据沙盒中已下载文件大小发送网络请求。否则取消后再继续下载,能够下载数据,但下载完的视频质量非常差
    self.dataTask = nil;
    self.currentSize = (NSInteger )nil;
}

//跳转
- (IBAction)push {

    //释放回话对象,否则会出现循环引用;或者在viewWillDisappear中释放
    [self.session invalidateAndCancel];
    self.session = nil;

    [UIApplication sharedApplication].keyWindow.rootViewController = nil;
}
#pragma mark -----------------
#pragma mark NSURLSessionDataDelegate
/*1.当接收到服务器响应的时候调用*/
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
    if (!_totalSize) {
        //1.拿到文件的总大小
        self.totalSize = response.expectedContentLength;

        //2.把文件的总大小以写文件的方式保存到沙盒中
        [self saveTotalSize];
    }

    //判断输出流是否存在,不存在才创建
    if (!_stream) {

        //3.拼接文件的全路径
        NSString *fullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:KfileName];

        //4.创建输出流
        NSOutputStream *stream = [[NSOutputStream alloc]initToFileAtPath:fullPath append:YES];
        self.stream = stream;


    //5.打开输出流
    [self.stream open];
}
    //6.通过该block告诉系统要如何处理服务器返回给我们的数据
    /*
     NSURLSessionResponseCancel = 0, //取消,不接受数据
     NSURLSessionResponseAllow = 1, //接收
     NSURLSessionResponseBecomeDownload = 2,  //变成下载请求
     NSURLSessionResponseBecomeStream //变成stream
     */
    completionHandler(NSURLSessionResponseAllow);

}

/*2.当接收到服务器返回的数据的时候调用,可能被调用多次*/
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    //1.使用输出流往文件中写数据
    [self.stream write:data.bytes maxLength:data.length];

    //2.计算文件下载进度
    self.currentSize +=data.length;

    //3.显示进度
    self.progressView.progress = 1.0 * self.currentSize/self.totalSize;
}

/*3.当请求结束的时候调用,如果请求失败,那么error有值*/
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    NSLog(@"didCompleteWithError");
    //关闭输出流
    [self.stream close];
    self.stream = nil;

}

你可能感兴趣的:(离线断点下载)