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 以处理它返回的数据?