之前用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。
首先用一个单例类来管理下载,单例类并不存储下载内容的数据,数据只在函数之间传递。
<p class="p1"><span class="s1">/**</span></p><p class="p1"><span class="s1"> * </span><span class="s2">单例</span></p><p class="p1"><span class="s1"> *</span></p><p class="p1"><span class="s1"> * @return </span><span class="s2">返回单例对象</span></p><p class="p1"><span class="s1"> */</span></p><p class="p2"><span class="s1">+ (</span><span class="s3">instancetype</span><span class="s1">)sharedInstance;</span></p><p class="p3"><span class="s1"></span> </p><p class="p1"><span class="s1">/**</span></p><p class="p4"><span class="s4"> * </span><span class="s1">开启任务下载资源</span></p><p class="p1"><span class="s1"> *</span></p><p class="p1"><span class="s1"> * @param model </span><span class="s2">下载参数</span></p><p class="p1"><span class="s1"> * @param progressBlock </span><span class="s2">回调下载进度</span></p><p class="p1"><span class="s1"> * @param stateBlock </span><span class="s2">下载状态</span></p><p class="p1"><span class="s1"> */</span></p><p class="p2"><span class="s1">- (</span><span class="s3">void</span><span class="s1">)download:(</span><span class="s5">DownloadModel</span><span class="s1"> *)model progress:(</span><span class="s3">void</span><span class="s1">(^)(</span><span class="s6">NSInteger</span><span class="s1"> receivedSize, </span><span class="s6">NSInteger</span><span class="s1"> expectedSize, </span><span class="s6">CGFloat</span><span class="s1"> progress))progressBlock state:(</span><span class="s3">void</span><span class="s1">(^)(</span><span class="s5">DownloadState</span><span class="s1"> state))stateBlock;</span></p><p class="p3"><span class="s1"></span> </p>
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实现的,也是最近改到项目里的,写代码就是边开发边重构吗,学以致用。