ios:NSURLSessionDataTask做文件断点下载

Posted qishiai819

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ios:NSURLSessionDataTask做文件断点下载相关的知识,希望对你有一定的参考价值。

之前用afn2.x的AFHttpOperation结合sqlite数据库管理做了文件的断点下载功能,之后苹果宣布要开始限制ipv4,不过AFN的东西时给予high-level的APIs的,因此不需要修改,但是国外的开发者建议使用AFN3.0版本。

闲来无事就想重新集成一下,迁移AFN3.0的时候因为没有了HTTPOperation,所以在修改代码的时候全部用NSURLSessionDowonloadTask代替,不过由于之前的数据库逻辑已经定型,且多处使用,修改起来比较复杂,DownloadTask是先下载临时文件,下载完成后再迁移到指定文件夹,并不能通过range来指定下载位置的起始,如果用户直接杀死App,又需要记住resumeData来重新下载,这样在多线程同时下载多个的时候集成出了问题,可能是我逻辑没有屡通,总觉的这样修改起来比较费力。

最后我完全摒弃了AFN,改而实用系统提供的URLSession和URLSessionDataTask及它的代理方法来实现,这样不需要修改现存的数据库逻辑,只需要修改下载暂停继续这部分的控制。

感谢大神提供:https://github.com/HHuiHao/HSDownloadManager。

首先用一个单例类来管理下载,单例类并不存储下载内容的数据,数据只在函数之间传递。

@interface TTDownloadManager : NSObject

/**
 *  单例
 *
 *  @return 返回单例对象
 */
+ (instancetype)sharedInstance;

/**
 *  开启任务下载资源
 *
 *  @param model         下载参数
 *  @param progressBlock 回调下载进度
 *  @param stateBlock    下载状态
 */
- (void)download:(DownloadModel *)model progress:(void(^)(NSInteger receivedSize, NSInteger expectedSize, CGFloat progress))progressBlock state:(void(^)(DownloadState state))stateBlock;



model包含下载文件的参数,对应创建的sqlite数据库的表,可以在内重组文件名,从而保证拿到已下载大小的range,重新创建task继续下载,我使用的方法也是居于上面的git连接修改的,因为要适用自己的项目,其他大概都差不多,大家可以先看看git项目,相信可以满足大家的大部分需求。


在作者的session类中,我也添加了doanloadModel的引用,从而方便在代理方法中拿到文件路径相关的参数做比较及存值。

@property (nonatomic, copy) NSString *fileName;


@property (nonatomic,strong)DownloadModel *model;

文笔拙劣,其实写个博客也是想为了给自己留个笔记,方便以后有用的话不需要在翻来翻去。直接贴代码吧,还惦记着NBA总决赛呢偷笑,大家可以参考我上面给的git项目地址,作者写的很好。

TTDownloadManager

//
//  TTDownloadManager.h
//  DownloadDemo
//
//  Created by qihb on 16/6/3.
//  Copyright © 2016年 Qihb. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "TTSessionModel.h"
#import "DownloadModel.h"

@interface TTDownloadManager : NSObject

/**
 *  单例
 *
 *  @return 返回单例对象
 */
+ (instancetype)sharedInstance;

/**
 *  开启任务下载资源
 *
 *  @param model         下载参数
 *  @param progressBlock 回调下载进度
 *  @param stateBlock    下载状态
 */
- (void)download:(DownloadModel *)model progress:(void(^)(NSInteger receivedSize, NSInteger expectedSize, CGFloat progress))progressBlock state:(void(^)(DownloadState state))stateBlock;

/**
 *  查询该资源的下载进度值
 *
 *  @param url 下载地址
 *
 *  @return 返回下载进度值
 */
- (CGFloat)progress:(NSString *)fileName;

/**
 *  获取该资源总大小
 *
 *  @param url 下载地址
 *
 *  @return 资源总大小
 */
- (NSInteger)fileTotalLength:(NSString *)url;

/**
 *  判断该资源是否下载完成
 *
 *  @param url 下载地址
 *
 *  @return YES: 完成
 */
- (BOOL)isCompletion:(NSString *)url;

/**
 *  删除该资源
 *
 *  @param url 下载地址
 */
- (void)deleteFile:(NSString *)url;

/**
 *  清空所有下载资源
 */
- (void)deleteAllFile;




/**
 *  暂停所有下载
 */
-(void)pauseAllTask;


/**
 *  取消下载
 *
 */
-(void)cancelTaskWithModel:(DownloadModel *)model;



@end

.m

//
//  TTDownloadManager.m
//  DownloadDemo
//
//  Created by qihb on 16/6/3.
//  Copyright © 2016年 Qihb. All rights reserved.
//

#import "TTDownloadManager.h"
#import "NSString+Hash.h"
// 缓存主目录
#define TTCachesDirectory [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"TTCache"]

// 保存文件名
#define TTFileKey(url) url.md5String


// 文件的存放路径(caches)
#define TTFileFullpath(url) [TTCachesDirectory stringByAppendingPathComponent:TTFileName(url)]

// 文件的已下载长度
#define TTDownloadLength(name) [[[NSFileManager defaultManager] attributesOfItemAtPath:SAVE_MODEL_PATH(name) error:nil][NSFileSize] integerValue]

// 存储文件总长度的文件路径(caches)
#define TTTotalLengthFullpath [TTCachesDirectory stringByAppendingPathComponent:@"totalLength.plist"]





@interface TTDownloadManager ()<NSCopying, NSURLSessionDelegate>

/** 保存所有任务(注:用md5后作为key) */
@property (nonatomic, strong) NSMutableDictionary *tasks;
/** 保存所有下载相关信息 */
@property (nonatomic, strong) NSMutableDictionary *sessionModels;

@end




@implementation TTDownloadManager

- (NSMutableDictionary *)tasks
{
    if (!_tasks) {
        _tasks = [NSMutableDictionary dictionary];
    }
    return _tasks;
}

- (NSMutableDictionary *)sessionModels
{
    if (!_sessionModels) {
        _sessionModels = [NSMutableDictionary dictionary];
    }
    return _sessionModels;
    
}


static TTDownloadManager *_downloadManager;

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    
    dispatch_once(&onceToken, ^{
        _downloadManager = [super allocWithZone:zone];
    });
    
    return _downloadManager;
}

- (nonnull id)copyWithZone:(nullable NSZone *)zone
{
    return _downloadManager;
}

+ (instancetype)sharedInstance
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _downloadManager = [[self alloc] init];
    });
    
    return _downloadManager;
}

/**
 *  创建缓存目录文件
 */
- (void)createCacheDirectory
{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if (![fileManager fileExistsAtPath:TTCachesDirectory]) {
        [fileManager createDirectoryAtPath:TTCachesDirectory withIntermediateDirectories:YES attributes:nil error:NULL];
    }
}

/**
 *  开启任务下载资源
 */
- (void)download:(DownloadModel *)model progress:(void (^)(NSInteger, NSInteger, CGFloat))progressBlock state:(void (^)(DownloadState))stateBlock
{
    NSString *model_url = model.model_url;
    NSString *fileName = [NSString stringWithFormat:@"%@.%@",model.model_md5_str,model.model_format];
    
    if (!model_url) return;
    if ([self isCompletion:fileName]) {
        stateBlock(DownloadStateCompleted);
        NSLog(@"----该资源已下载完成");
        return;
    }
    
    // 暂停
    if ([self.tasks valueForKey:fileName]) {
        [self handle:fileName];
        
        return;
    }
    
    // 创建缓存目录文件
    [self createCacheDirectory];
    
    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
    
    NSString *toPath = SAVE_MODEL_PATH(fileName);
    
    // 创建流
    NSOutputStream *stream = [NSOutputStream outputStreamToFileAtPath:toPath append:YES];
    
    // 创建请求
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:model_url]];
    
    // 设置请求头
    NSString *range = [NSString stringWithFormat:@"bytes=%zd-", TTDownloadLength(fileName)];
    [request setValue:range forHTTPHeaderField:@"Range"];
    
    // 创建一个Data任务
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request];
    NSTimeInterval time = [[NSDate date] timeIntervalSince1970];
    NSUInteger taskIdentifier = (unsigned long)time;
    [task setValue:@(taskIdentifier) forKeyPath:@"taskIdentifier"];
    
    // 保存任务
    [self.tasks setValue:task forKey:fileName];
    
    TTSessionModel *sessionModel = [[TTSessionModel alloc] init];
    sessionModel.url = model_url;
    sessionModel.fileName = fileName;
    sessionModel.model = model;
    sessionModel.progressBlock = progressBlock;
    sessionModel.stateBlock = stateBlock;
    sessionModel.stream = stream;
    [self.sessionModels setValue:sessionModel forKey:@(task.taskIdentifier).stringValue];
    
    [self start:fileName];
}


- (void)handle:(NSString *)key
{
    NSURLSessionDataTask *task = [self getTask:key];
    if (task.state == NSURLSessionTaskStateRunning) {
        [self pause:key];
    } else {
        [self start:key];
    }
}

/**
 *  开始下载
 */
- (void)start:(NSString *)key
{
    NSURLSessionDataTask *task = [self getTask:key];
    [task resume];
    
    [self getSessionModel:task.taskIdentifier].stateBlock(DownloadStateStart);
}

/**
 *  暂停下载
 */
- (void)pause:(NSString *)key
{
    NSURLSessionDataTask *task = [self getTask:key];
    [task suspend];
    
    [self getSessionModel:task.taskIdentifier].stateBlock(DownloadStateSuspended);
}

/**
 *  根据url获得对应的下载任务
 */
- (NSURLSessionDataTask *)getTask:(NSString *)key
{
    return (NSURLSessionDataTask *)[self.tasks valueForKey:key];
}

/**
 *  根据url获取对应的下载信息模型
 */
- (TTSessionModel *)getSessionModel:(NSUInteger)taskIdentifier
{
    return (TTSessionModel *)[self.sessionModels valueForKey:@(taskIdentifier).stringValue];
}

/**
 *  判断该文件是否下载完成
 */
- (BOOL)isCompletion:(NSString *)fileName
{
    if ([self fileTotalLength:fileName] && TTDownloadLength(fileName) == [self fileTotalLength:fileName]) {
        return YES;
    }
    return NO;
}

/**
 *  查询该资源的下载进度值
 */
- (CGFloat)progress:(NSString *)fileName
{
    return [self fileTotalLength:fileName] == 0 ? 0.0 : 1.0 * TTDownloadLength(fileName) /  [self fileTotalLength:fileName];
}

/**
 *  获取该资源总大小
 */
- (NSInteger)fileTotalLength:(NSString *)fileName
{
    return [[NSDictionary dictionaryWithContentsOfFile:TTTotalLengthFullpath][fileName] integerValue];
}

#pragma mark - 删除
/**
 *  删除该资源
 */
- (void)deleteFile:(NSString *)fileName
{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    if ([fileManager fileExistsAtPath:SAVE_MODEL_PATH(fileName)]) {
        
        // 删除沙盒中的资源
        [fileManager removeItemAtPath:SAVE_MODEL_PATH(fileName) error:nil];
        // 删除任务
        [self.tasks removeObjectForKey:fileName];
        [self.sessionModels removeObjectForKey:@([self getTask:fileName].taskIdentifier).stringValue];
        // 删除资源总长度
        if ([fileManager fileExistsAtPath:TTTotalLengthFullpath]) {
            
            NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:TTTotalLengthFullpath];
            [dict removeObjectForKey:fileName];
            [dict writeToFile:TTTotalLengthFullpath atomically:YES];
            
        }
    }
}



/**
 *  暂停所有下载
 */
-(void)pauseAllTask{
    NSArray *allKeys = [self.tasks allKeys];
    if (allKeys&&allKeys.count>0) {
        for (int i=0; i<allKeys.count; i++) {
            NSString *key = [allKeys objectAtIndex:i];
            NSURLSessionDataTask *task = [self getTask:key];
            [task suspend];
            TTSessionModel *sessionModel =[self getSessionModel:task.taskIdentifier];
            sessionModel.model.model_download_flag = @"2";
            [sessionModel.model saveOrUpdate];
        }
    }
}


/**
 *  取消下载
 *
 */
-(void)cancelTaskWithModel:(DownloadModel *)model{
    NSString *key = [NSString stringWithFormat:@"%@.%@",model.model_md5_str,model.model_format];
    NSURLSessionDataTask *task = [self getTask:key];
    [task cancel];
    
    [self deleteFile:key];
}




#pragma mark - 代理
#pragma mark NSURLSessionDataDelegate
/**
 * 接收到响应
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSHTTPURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
    
    TTSessionModel *sessionModel = [self getSessionModel:dataTask.taskIdentifier];
    
    // 打开流
    [sessionModel.stream open];
    
    
    // 获得服务器这次请求 返回数据的总长度
    NSInteger totalLength = [response.allHeaderFields[@"Content-Length"] integerValue] + TTDownloadLength(sessionModel.fileName);
    sessionModel.totalLength = totalLength;
    
    // 存储总长度
    NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:TTTotalLengthFullpath];
    if (dict == nil) dict = [NSMutableDictionary dictionary];
    dict[sessionModel.fileName] = @(totalLength);
    [dict writeToFile:TTTotalLengthFullpath atomically:YES];
    
    // 接收这个请求,允许接收服务器的数据
    completionHandler(NSURLSessionResponseAllow);
}

/**
 * 接收到服务器返回的数据
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    TTSessionModel *sessionModel = [self getSessionModel:dataTask.taskIdentifier];
    
    // 写入数据
    [sessionModel.stream write:(uint8_t *)data.bytes maxLength:data.length];
    
    // 下载进度
    NSUInteger receivedSize = TTDownloadLength(sessionModel.fileName);
    NSUInteger expectedSize = sessionModel.totalLength;
    CGFloat progress = 1.0 * receivedSize / expectedSize;
    
    sessionModel.progressBlock(receivedSize, expectedSize, progress);
}

/**
 * 请求完毕(成功|失败)
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    TTSessionModel *sessionModel = [self getSessionModel:task.taskIdentifier];
    if (!sessionModel) return;
    
    if ([self isCompletion:sessionModel.fileName]) {
        // 下载完成
        sessionModel.stateBlock(DownloadStateCompleted);
    } else if (error){
        
        if (error.code == NSURLErrorTimedOut||error.code == NSURLErrorNetworkConnectionLost) {
            sessionModel.stateBlock(DownloadStateSuspended);
        }else{
        
        // 下载失败
            sessionModel.stateBlock(DownloadStateUnBegin);
        }
    }
    
    // 关闭流
    [sessionModel.stream close];
    sessionModel.stream = nil;
    
    // 清除任务
    [self.tasks removeObjectForKey:sessionModel.fileName];
    [self.sessionModels removeObjectForKey:@(task.taskIdentifier).stringValue];
}



@end

TTSessionModel

//
//  TTSessionModel.h
//  DownloadDemo
//
//  Created by qihb on 16/6/3.
//  Copyright © 2016年 Qihb. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import "DownloadModel.h"


typedef enum {
    /** 未下载 */
    DownloadStateUnBegin = 0,
    /** 下载中 */
    DownloadStateStart,
    /** 下载暂停 */
    DownloadStateSuspended,
    /** 下载完成 */
    DownloadStateCompleted,
}DownloadState;

//0未下载  1正在下载  2暂停下载 3已完成下载


@interface TTSessionModel : NSObject

/** 流 */
@property (nonatomic, strong) NSOutputStream *stream;

/** 下载地址 */
@property (nonatomic, copy) NSString *url;


@property (nonatomic, copy) NSString *fileName;


@property (nonatomic,strong)DownloadModel *model;


/** 获得服务器这次请求 返回数据的总长度 */
@property (nonatomic, assign) NSInteger totalLength;

/** 下载进度 */
@property (nonatomic, copy) void(^progressBlock)(NSInteger receivedSize, NSInteger expectedSize, CGFloat progress);

/** 下载状态 */
@property (nonatomic, copy) void(^stateBlock)(DownloadState state);

@end

DownloadModel


//
//  DownloadModelList.h
//  Up Studio
//
//  Created by qihb on 16/5/12.
//  Copyright © 2016年 gjh. All rights reserved.
//

#import "DBBaseModel.h"

#define dbModelKeyWord @"model_md5_str"

@interface DownloadModel : DBBaseModel

@property(nonatomic,copy)NSString *model_md5_str;           //md5串
@property(nonatomic,copy)NSString *model_image_url;         //图片远程下载地址
@property(nonatomic,copy)NSString *model_url;               //模型远程下载地址
@property(nonatomic,copy)NSString *model_size;              //文件大小
@property(nonatomic,copy)NSString *model_name;              //名字
@property(nonatomic,copy)NSString *model_format;            //格式 up3/stl
@property(nonatomic,copy)NSString *model_type;              //分类
@property(nonatomic,copy)NSString *model_image_path;        //图片本地路径
@property(nonatomic,copy)NSString *model_path;              //模型本地路径
@property(nonatomic,copy)NSString *model_subModel_num;      //子模型数量
@property(nonatomic,copy)NSString *model_payyed_flag;       //付款标识
@property(nonatomic,copy)NSString *model_price;             //价格
@property(nonatomic,copy)NSString *model_copyright;         //版权
@property(nonatomic,copy)NSString *model_from_source;       //来源 0预设 1下载 2本地保存
@property(nonatomic,copy)NSString *model_download_flag;     //0未下载  1正在下载  2暂停下载 3已完成下载
@property(nonatomic,copy)NSString *model_download_percentage;//下载百分比
@property(nonatomic,copy)NSString *recent_time;             //最近一次使用时间

//获取所有下载完成的模型数据
+(NSArray *)findAllDownloadFinished;


@end


downloadModel的部分大家可以参照我之前的博客,数据库DataBaseQueue多线程安全的博客,这个就是继承DBBaseModel实现的,也是最近改到项目里的,写代码就是边开发边重构吗,学以致用。





以上是关于ios:NSURLSessionDataTask做文件断点下载的主要内容,如果未能解决你的问题,请参考以下文章

iOS https请求 NSURLSessionDataTask

ios开发网络学习十一:NSURLSessionDataTask离线断点下载(断点续传)

如何将“C++ 完成处理程序”传递给 NSURLSessionDataTask 以处理它返回的数据?

非唯一 NSURLSessionDataTask taskIdentifiers

检查 NSURLSessionDataTask 的响应值

iOS —— NNSURLSessionDataTask