// // RQDownloadManager.m // TEST // // Created by 张嵘 on 2018/10/22. // Copyright © 2018 张嵘. All rights reserved. // #import "RQDownloadManager.h" #import "AppDelegate.h" @interface RQDownloadManager () @property (nonatomic, strong) NSURLSession *session; // NSURLSession @property (nonatomic, strong) NSMutableDictionary *dataTaskDic; // 同时下载多个文件,需要创建多个NSURLSessionDownloadTask,用该字典来存储 @property (nonatomic, assign) NSInteger currentCount; // 当前正在下载的个数 @property (nonatomic, assign) NSInteger maxConcurrentCount; // 最大同时下载数量 @property (nonatomic, assign) BOOL allowsCellularAccess; // 是否允许蜂窝网络下载 @end @implementation RQDownloadManager #pragma mark - 单例相关 static id instace = nil; + (id)allocWithZone:(struct _NSZone *)zone { if (instace == nil) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instace = [super allocWithZone:zone]; // 初始化 [instace initBase]; // 添加未捕获异常的监听 [instace handleUncaughtExreption]; // 添加监听 [instace addObservers]; // 开启网络监听 [[RQNetworkReachabilityManager shareManager] monitorNetworkStatus]; }); } return instace; } - (instancetype)init { return instace; } + (instancetype)sharedManager { return [[self alloc] init]; } - (id)copyWithZone:(struct _NSZone *)zone { return instace; } - (id)mutableCopyWithZone:(struct _NSZone *)zone { return instace; } #pragma mark - 单例初始化调用 - (void)initBase { // 初始化 _currentCount = 0; _maxConcurrentCount = [[NSUserDefaults standardUserDefaults] integerForKey:RQDownloadMaxConcurrentCountKey]; _allowsCellularAccess = [[NSUserDefaults standardUserDefaults] boolForKey:RQDownloadAllowsCellularAccessKey]; _dataTaskDic = [NSMutableDictionary dictionary]; // 单线程代理队列 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; queue.maxConcurrentOperationCount = 1; // 后台下载标识 NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"RQDownloadBackgroundSessionIdentifier"]; // 允许蜂窝网络下载,默认为YES,这里开启,我们添加了一个变量去控制用户切换选择 configuration.allowsCellularAccess = YES; // 创建NSURLSession,配置信息、代理、代理线程 _session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:queue]; } /** * 添加监听 */ - (void)addObservers { [[NSNotificationCenter defaultCenter] addObserver:instace selector:@selector(openDownloadTask) name:UIApplicationDidFinishLaunchingNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:instace selector:@selector(applicationWillTerminate) name:UIApplicationWillTerminateNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:instace selector:@selector(applicationWillTerminate) name:kNotificationUncaughtException object:nil]; // 最大下载并发数变更通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(downloadMaxConcurrentCountChange:) name:RQDownloadMaxConcurrentCountChangeNotification object:nil]; // 是否允许蜂窝网络下载改变通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(downloadAllowsCellularAccessChange:) name:RQDownloadAllowsCellularAccessChangeNotification object:nil]; // 网路改变通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkingReachabilityDidChange:) name:RQNetworkingReachabilityDidChangeNotification object:nil]; } /** * 添加未捕获异常的监听 */ - (void)handleUncaughtExreption { [RQUncaughtExceptionHandler setDefaultHandler]; } #pragma mark - Private Method // 加入准备下载任务 - (void)startDownloadTask:(RQDownloadModel *)model { // 取出数据库中模型数据,如果不存在,添加到数据空中 RQDownloadModel *downloadModel = [[RQDataBaseManager shareManager] getModelWithUrl:model.urlString]; if (!downloadModel) { downloadModel = model; [[RQDataBaseManager shareManager] insertModel:downloadModel]; } // 更新状态为等待下载 downloadModel.status = RQDownloadStatus_Waiting; [[RQDataBaseManager shareManager] updateWithModel:downloadModel option:RQDBUpdateOptionAllParam]; // 下载 if (_currentCount < self.maxConcurrentCount && [self networkingAllowsDownloadTask]) [self downloadwithModel:downloadModel]; } // 开始下载 - (void)downloadwithModel:(RQDownloadModel *)model { // 更新状态为开始 model.status = RQDownloadStatus_Running; [[RQDataBaseManager shareManager] updateWithModel:model option:RQDBUpdateOptionAllParam]; _currentCount++; // 创建NSURLSessionDownloadTask NSURLSessionDownloadTask *downloadTask; if (model.resumeData) { CGFloat version = [[[UIDevice currentDevice] systemVersion] floatValue]; if (version >= 10.0 && version < 10.2) { downloadTask = [_session downloadTaskWithCorrectResumeData:model.resumeData]; }else { downloadTask = [_session downloadTaskWithResumeData:model.resumeData]; } }else { downloadTask = [_session downloadTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:model.urlString]]]; } // 添加描述标签 downloadTask.taskDescription = model.urlString; // 更新存储的NSURLSessionDownloadTask对象 [_dataTaskDic setValue:downloadTask forKey:model.urlString]; // 启动(继续下载) [downloadTask resume]; } // 暂停下载 - (void)pauseDownloadTask:(RQDownloadModel *)model { // 取最新数据 RQDownloadModel *downloadModel = [[RQDataBaseManager shareManager] getModelWithUrl:model.urlString]; // 取消任务 [self cancelTaskWithModel:downloadModel delete:NO]; // 更新数据库状态为暂停 downloadModel.status = RQDownloadStatus_Suspended; [[RQDataBaseManager shareManager] updateWithModel:downloadModel option:RQDBUpdateOptionAllParam]; } // 删除下载任务及本地缓存 - (void)deleteTaskAndCache:(RQDownloadModel *)model { // 删除本地缓存、数据库数据 dispatch_async(dispatch_get_global_queue(0, 0), ^{ [[NSFileManager defaultManager] removeItemAtPath:model.destinationPath error:nil]; model.status = RQDownloadStatus_None; model.statusText = nil; [[RQDataBaseManager shareManager] updateWithModel:model option:RQDBUpdateOptionAllParam]; [[RQDataBaseManager shareManager] deleteModelWithUrl:model.urlString]; [self clearTmpDirectory]; }); // 如果正在下载,取消任务 [self cancelTaskWithModel:model delete:YES]; } // 清除沙盒临时文件 - (void)clearTmpDirectory { NSArray* tmpDirectory = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:NSTemporaryDirectory() error:NULL]; for (NSString *file in tmpDirectory) { [[NSFileManager defaultManager] removeItemAtPath:[NSString stringWithFormat:@"%@%@", NSTemporaryDirectory(), file] error:NULL]; } } // 取消任务 - (void)cancelTaskWithModel:(RQDownloadModel *)model delete:(BOOL)delete { if (model.status == RQDownloadStatus_Running) { // 获取NSURLSessionDownloadTask NSURLSessionDownloadTask *downloadTask = [_dataTaskDic valueForKey:model.urlString]; // 取消任务 [downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) { // 更新下载数据 model.resumeData = delete? nil : resumeData; [[RQDataBaseManager shareManager] updateWithModel:model option:RQDBUpdateOptionResumeData]; // 更新当前正在下载的个数 if (_currentCount > 0) _currentCount--; // 开启等待下载任务 [self startDownloadWaitingTask]; }]; // 移除字典存储的对象 if (delete) { [_dataTaskDic removeObjectForKey:model.urlString]; } } } // 开启等待下载任务 - (void)startDownloadWaitingTask { if (_currentCount < self.maxConcurrentCount && [self networkingAllowsDownloadTask]) { // 获取下一条等待的数据 RQDownloadModel *model = [[RQDataBaseManager shareManager] getWaitingModel]; if (model) { // 下载 [self downloadwithModel:model]; // 递归,开启下一个等待任务 [self startDownloadWaitingTask]; } } } // 下载时,杀死进程,更新所有正在下载的任务为等待 - (void)updateDownloadingTaskState { NSArray *downloadingData = [[RQDataBaseManager shareManager] getAllDownloadingData]; for (RQDownloadModel *model in downloadingData) { model.status = RQDownloadStatus_Waiting; [[RQDataBaseManager shareManager] updateWithModel:model option:RQDBUpdateOptionState]; } } // 重启时开启等待下载的任务 - (void)openDownloadTask { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self startDownloadWaitingTask]; }); } // 停止正在下载任务为等待状态 - (void)pauseDownloadingTaskWithAll:(BOOL)all { // 获取正在下载的数据 NSArray *downloadingData = [[RQDataBaseManager shareManager] getAllDownloadingData]; NSInteger count = all ? downloadingData.count : downloadingData.count - self.maxConcurrentCount; for (NSInteger i = 0; i < count; i++) { // 取消任务 RQDownloadModel *model = downloadingData[i]; [self cancelTaskWithModel:model delete:NO]; // 更新状态为等待 model.status = RQDownloadStatus_Waiting; [[RQDataBaseManager shareManager] updateWithModel:model option:RQDBUpdateOptionAllParam]; } } - (void)applicationWillTerminate { [self updateDownloadingTaskState]; } #pragma mark - NSURLSessionDownloadDelegate // 接收到服务器返回数据,会被调用多次 - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { // 获取模型 RQDownloadModel *model = [[RQDataBaseManager shareManager] getModelWithUrl:downloadTask.taskDescription]; // 更新当前下载大小 model.fileDownloadSize = totalBytesWritten; model.fileTotalSize = totalBytesExpectedToWrite; // 计算速度时间内下载文件的大小 model.intervalFileSize += bytesWritten; // 获取上次计算时间与当前时间间隔 NSInteger intervals = [RQ_SHARE_FUNCTION getIntervalsWithTimeStamp:model.lastSpeedTime]; if (intervals >= 1) { // 计算速度 model.speed = model.intervalFileSize / intervals; // 重置变量 model.intervalFileSize = 0; model.lastSpeedTime = [RQ_SHARE_FUNCTION getTimeStampWithDate:[NSDate date]]; } // 计算进度 double byts = totalBytesWritten * 1.0 / 1024 /1024; double total = totalBytesExpectedToWrite * 1.0 / 1024 /1024; NSString *text = [NSString stringWithFormat:@"%.1lfMB/%.1lfMB",byts,total]; CGFloat progress = 1.0 * byts / total; model.statusText = text; model.progress = progress; NSLog(@"%@",text); // 更新数据库中数据 [[RQDataBaseManager shareManager] updateWithModel:model option:RQDBUpdateOptionAllParam]; // 进度通知 [[NSNotificationCenter defaultCenter] postNotificationName:RQDownloadProgressNotification object:model]; } // 下载完成 - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { // 获取模型 RQDownloadModel *model = [[RQDataBaseManager shareManager] getModelWithUrl:downloadTask.taskDescription]; // 移动文件,原路径文件由系统自动删除 NSError *error = nil; if (model) { [[NSFileManager defaultManager] moveItemAtPath:[location path] toPath:model.destinationPath error:&error]; if (error) NSLog(@"下载完成,移动文件发生错误:%@", error); } } #pragma mark - NSURLSessionTaskDelegate // 请求完成 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { // 调用cancel方法直接返回,在相应操作是直接进行处理 if (error && [error.localizedDescription isEqualToString:@"cancelled"]) return; // 获取模型 RQDownloadModel *model = [[RQDataBaseManager shareManager] getModelWithUrl:task.taskDescription]; // 下载时,进程杀死,重新启动,回调错误 if (error && [error.userInfo objectForKey:NSURLErrorBackgroundTaskCancelledReasonKey]) { model.status = RQDownloadStatus_Waiting; model.resumeData = [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData]; [[RQDataBaseManager shareManager] updateWithModel:model option:RQDBUpdateOptionAllParam]; return; } // 更新下载数据、任务状态 if (error) { model.status = RQDownloadStatus_Failed; model.resumeData = [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData]; [[RQDataBaseManager shareManager] updateWithModel:model option:RQDBUpdateOptionAllParam]; }else { model.status = RQDownloadStatus_Completed; } // 更新数据 if (_currentCount > 0) _currentCount--; if (model.urlString) { [_dataTaskDic removeObjectForKey:model.urlString]; } // 更新数据库状态 [[RQDataBaseManager shareManager] updateWithModel:model option:RQDBUpdateOptionAllParam]; // 开启等待下载任务 [self startDownloadWaitingTask]; NSLog(@"\n 文件:%@,下载完成 \n 本地路径:%@ \n 错误:%@ \n", model.fileName, model.destinationPath, error); } #pragma mark - NSURLSessionDelegate // 应用处于后台,所有下载任务完成及NSURLSession协议调用之后调用 - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session { dispatch_async(dispatch_get_main_queue(), ^{ AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate]; if (appDelegate.backgroundSessionCompletionHandler) { void (^completionHandler)(void) = appDelegate.backgroundSessionCompletionHandler; appDelegate.backgroundSessionCompletionHandler = nil; // 执行block,系统后台生成快照,释放阻止应用挂起的断言 completionHandler(); } }); } #pragma mark - RQDownloadMaxConcurrentCountChangeNotification - (void)downloadMaxConcurrentCountChange:(NSNotification *)notification { self.maxConcurrentCount = [notification.object integerValue]; if (_currentCount < self.maxConcurrentCount) { // 当前下载数小于并发数,开启等待下载任务 [self startDownloadWaitingTask]; }else if (_currentCount > self.maxConcurrentCount) { // 变更正在下载任务为等待下载 [self pauseDownloadingTaskWithAll:NO]; } } #pragma mark - RQDownloadAllowsCellularAccessChangeNotification - (void)downloadAllowsCellularAccessChange:(NSNotification *)notification { _allowsCellularAccess = [notification.object boolValue]; [self allowsCellularAccessOrNetworkingReachabilityDidChangeAction]; } #pragma mark - RQNetworkingReachabilityDidChangeNotification - (void)networkingReachabilityDidChange:(NSNotification *)notification { [self allowsCellularAccessOrNetworkingReachabilityDidChangeAction]; } // 是否允许蜂窝网络下载或网络状态变更事件 - (void)allowsCellularAccessOrNetworkingReachabilityDidChangeAction { if ([[RQNetworkReachabilityManager shareManager] networkReachabilityStatus] == AFNetworkReachabilityStatusNotReachable) { // 无网络,暂停正在下载任务 [self pauseDownloadingTaskWithAll:YES]; }else { if ([self networkingAllowsDownloadTask]) { // 开启等待任务 [self startDownloadWaitingTask]; }else { // 增加一个友善的提示,蜂窝网络情况下如果有正在下载,提示已暂停 if ([kRQDownloadDataBaseManager getLastDownloadingModel]) { ShowMsg(@"当前为蜂窝网络,已停止下载任务,可在设置中开启"); } // 当前为蜂窝网络,不允许下载,暂停正在下载任务 [self pauseDownloadingTaskWithAll:YES]; } } } // 是否允许下载任务 - (BOOL)networkingAllowsDownloadTask { // 当前网络状态 AFNetworkReachabilityStatus status = [[RQNetworkReachabilityManager shareManager] networkReachabilityStatus]; // 无网络 或 (当前为蜂窝网络,且不允许蜂窝网络下载) // if (status == AFNetworkReachabilityStatusNotReachable || (status == AFNetworkReachabilityStatusReachableViaWWAN && !_allowsCellularAccess)) { // return NO; // } if (status == AFNetworkReachabilityStatusNotReachable) { return NO; } return YES; } - (void)dealloc { [_session invalidateAndCancel]; [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma mark - LazyLoad @end