RQDownloadManager.m 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. //
  2. // RQDownloadManager.m
  3. // TEST
  4. //
  5. // Created by 张嵘 on 2018/10/22.
  6. // Copyright © 2018 张嵘. All rights reserved.
  7. //
  8. #import "RQDownloadManager.h"
  9. #import "AppDelegate.h"
  10. @interface RQDownloadManager () <NSURLSessionDelegate, NSURLSessionDownloadDelegate>
  11. @property (nonatomic, strong) NSURLSession *session; // NSURLSession
  12. @property (nonatomic, strong) NSMutableDictionary *dataTaskDic; // 同时下载多个文件,需要创建多个NSURLSessionDownloadTask,用该字典来存储
  13. @property (nonatomic, assign) NSInteger currentCount; // 当前正在下载的个数
  14. @property (nonatomic, assign) NSInteger maxConcurrentCount; // 最大同时下载数量
  15. @property (nonatomic, assign) BOOL allowsCellularAccess; // 是否允许蜂窝网络下载
  16. @end
  17. @implementation RQDownloadManager
  18. #pragma mark - 单例相关
  19. static id instace = nil;
  20. + (id)allocWithZone:(struct _NSZone *)zone {
  21. if (instace == nil) {
  22. static dispatch_once_t onceToken;
  23. dispatch_once(&onceToken, ^{
  24. instace = [super allocWithZone:zone];
  25. // 初始化
  26. [instace initBase];
  27. // 添加未捕获异常的监听
  28. [instace handleUncaughtExreption];
  29. // 添加监听
  30. [instace addObservers];
  31. // 开启网络监听
  32. [[RQNetworkReachabilityManager shareManager] monitorNetworkStatus];
  33. });
  34. }
  35. return instace;
  36. }
  37. - (instancetype)init {
  38. return instace;
  39. }
  40. + (instancetype)sharedManager {
  41. return [[self alloc] init];
  42. }
  43. - (id)copyWithZone:(struct _NSZone *)zone {
  44. return instace;
  45. }
  46. - (id)mutableCopyWithZone:(struct _NSZone *)zone {
  47. return instace;
  48. }
  49. #pragma mark - 单例初始化调用
  50. - (void)initBase {
  51. // 初始化
  52. _currentCount = 0;
  53. _maxConcurrentCount = [[NSUserDefaults standardUserDefaults] integerForKey:RQDownloadMaxConcurrentCountKey];
  54. _allowsCellularAccess = [[NSUserDefaults standardUserDefaults] boolForKey:RQDownloadAllowsCellularAccessKey];
  55. _dataTaskDic = [NSMutableDictionary dictionary];
  56. // 单线程代理队列
  57. NSOperationQueue *queue = [[NSOperationQueue alloc] init];
  58. queue.maxConcurrentOperationCount = 1;
  59. // 后台下载标识
  60. NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"RQDownloadBackgroundSessionIdentifier"];
  61. // 允许蜂窝网络下载,默认为YES,这里开启,我们添加了一个变量去控制用户切换选择
  62. configuration.allowsCellularAccess = YES;
  63. // 创建NSURLSession,配置信息、代理、代理线程
  64. _session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:queue];
  65. }
  66. /**
  67. * 添加监听
  68. */
  69. - (void)addObservers {
  70. [[NSNotificationCenter defaultCenter] addObserver:instace selector:@selector(openDownloadTask) name:UIApplicationDidFinishLaunchingNotification object:nil];
  71. [[NSNotificationCenter defaultCenter] addObserver:instace selector:@selector(applicationWillTerminate) name:UIApplicationWillTerminateNotification object:nil];
  72. [[NSNotificationCenter defaultCenter] addObserver:instace selector:@selector(applicationWillTerminate) name:kNotificationUncaughtException object:nil];
  73. // 最大下载并发数变更通知
  74. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(downloadMaxConcurrentCountChange:) name:RQDownloadMaxConcurrentCountChangeNotification object:nil];
  75. // 是否允许蜂窝网络下载改变通知
  76. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(downloadAllowsCellularAccessChange:) name:RQDownloadAllowsCellularAccessChangeNotification object:nil];
  77. // 网路改变通知
  78. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkingReachabilityDidChange:) name:RQNetworkingReachabilityDidChangeNotification object:nil];
  79. }
  80. /**
  81. * 添加未捕获异常的监听
  82. */
  83. - (void)handleUncaughtExreption {
  84. [RQUncaughtExceptionHandler setDefaultHandler];
  85. }
  86. #pragma mark - Private Method
  87. // 加入准备下载任务
  88. - (void)startDownloadTask:(RQDownloadModel *)model {
  89. // 取出数据库中模型数据,如果不存在,添加到数据空中
  90. RQDownloadModel *downloadModel = [[RQDataBaseManager shareManager] getModelWithUrl:model.urlString];
  91. if (!downloadModel) {
  92. downloadModel = model;
  93. [[RQDataBaseManager shareManager] insertModel:downloadModel];
  94. }
  95. // 更新状态为等待下载
  96. downloadModel.status = RQDownloadStatus_Waiting;
  97. [[RQDataBaseManager shareManager] updateWithModel:downloadModel option:RQDBUpdateOptionAllParam];
  98. // 下载
  99. if (_currentCount < self.maxConcurrentCount && [self networkingAllowsDownloadTask]) [self downloadwithModel:downloadModel];
  100. }
  101. // 开始下载
  102. - (void)downloadwithModel:(RQDownloadModel *)model {
  103. // 更新状态为开始
  104. model.status = RQDownloadStatus_Running;
  105. [[RQDataBaseManager shareManager] updateWithModel:model option:RQDBUpdateOptionAllParam];
  106. _currentCount++;
  107. // 创建NSURLSessionDownloadTask
  108. NSURLSessionDownloadTask *downloadTask;
  109. if (model.resumeData) {
  110. CGFloat version = [[[UIDevice currentDevice] systemVersion] floatValue];
  111. if (version >= 10.0 && version < 10.2) {
  112. downloadTask = [_session downloadTaskWithCorrectResumeData:model.resumeData];
  113. }else {
  114. downloadTask = [_session downloadTaskWithResumeData:model.resumeData];
  115. }
  116. }else {
  117. downloadTask = [_session downloadTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:model.urlString]]];
  118. }
  119. // 添加描述标签
  120. downloadTask.taskDescription = model.urlString;
  121. // 更新存储的NSURLSessionDownloadTask对象
  122. [_dataTaskDic setValue:downloadTask forKey:model.urlString];
  123. // 启动(继续下载)
  124. [downloadTask resume];
  125. }
  126. // 暂停下载
  127. - (void)pauseDownloadTask:(RQDownloadModel *)model {
  128. // 取最新数据
  129. RQDownloadModel *downloadModel = [[RQDataBaseManager shareManager] getModelWithUrl:model.urlString];
  130. // 取消任务
  131. [self cancelTaskWithModel:downloadModel delete:NO];
  132. // 更新数据库状态为暂停
  133. downloadModel.status = RQDownloadStatus_Suspended;
  134. [[RQDataBaseManager shareManager] updateWithModel:downloadModel option:RQDBUpdateOptionAllParam];
  135. }
  136. // 删除下载任务及本地缓存
  137. - (void)deleteTaskAndCache:(RQDownloadModel *)model {
  138. // 删除本地缓存、数据库数据
  139. dispatch_async(dispatch_get_global_queue(0, 0), ^{
  140. [[NSFileManager defaultManager] removeItemAtPath:model.destinationPath error:nil];
  141. model.status = RQDownloadStatus_None;
  142. model.statusText = nil;
  143. [[RQDataBaseManager shareManager] updateWithModel:model option:RQDBUpdateOptionAllParam];
  144. [[RQDataBaseManager shareManager] deleteModelWithUrl:model.urlString];
  145. [self clearTmpDirectory];
  146. });
  147. // 如果正在下载,取消任务
  148. [self cancelTaskWithModel:model delete:YES];
  149. }
  150. // 清除沙盒临时文件
  151. - (void)clearTmpDirectory
  152. {
  153. NSArray* tmpDirectory = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:NSTemporaryDirectory() error:NULL];
  154. for (NSString *file in tmpDirectory) {
  155. [[NSFileManager defaultManager] removeItemAtPath:[NSString stringWithFormat:@"%@%@", NSTemporaryDirectory(), file] error:NULL];
  156. }
  157. }
  158. // 取消任务
  159. - (void)cancelTaskWithModel:(RQDownloadModel *)model delete:(BOOL)delete {
  160. if (model.status == RQDownloadStatus_Running) {
  161. // 获取NSURLSessionDownloadTask
  162. NSURLSessionDownloadTask *downloadTask = [_dataTaskDic valueForKey:model.urlString];
  163. // 取消任务
  164. [downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
  165. // 更新下载数据
  166. model.resumeData = delete? nil : resumeData;
  167. [[RQDataBaseManager shareManager] updateWithModel:model option:RQDBUpdateOptionResumeData];
  168. // 更新当前正在下载的个数
  169. if (_currentCount > 0) _currentCount--;
  170. // 开启等待下载任务
  171. [self startDownloadWaitingTask];
  172. }];
  173. // 移除字典存储的对象
  174. if (delete) {
  175. [_dataTaskDic removeObjectForKey:model.urlString];
  176. }
  177. }
  178. }
  179. // 开启等待下载任务
  180. - (void)startDownloadWaitingTask {
  181. if (_currentCount < self.maxConcurrentCount && [self networkingAllowsDownloadTask]) {
  182. // 获取下一条等待的数据
  183. RQDownloadModel *model = [[RQDataBaseManager shareManager] getWaitingModel];
  184. if (model) {
  185. // 下载
  186. [self downloadwithModel:model];
  187. // 递归,开启下一个等待任务
  188. [self startDownloadWaitingTask];
  189. }
  190. }
  191. }
  192. // 下载时,杀死进程,更新所有正在下载的任务为等待
  193. - (void)updateDownloadingTaskState {
  194. NSArray *downloadingData = [[RQDataBaseManager shareManager] getAllDownloadingData];
  195. for (RQDownloadModel *model in downloadingData) {
  196. model.status = RQDownloadStatus_Waiting;
  197. [[RQDataBaseManager shareManager] updateWithModel:model option:RQDBUpdateOptionState];
  198. }
  199. }
  200. // 重启时开启等待下载的任务
  201. - (void)openDownloadTask {
  202. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  203. [self startDownloadWaitingTask];
  204. });
  205. }
  206. // 停止正在下载任务为等待状态
  207. - (void)pauseDownloadingTaskWithAll:(BOOL)all {
  208. // 获取正在下载的数据
  209. NSArray *downloadingData = [[RQDataBaseManager shareManager] getAllDownloadingData];
  210. NSInteger count = all ? downloadingData.count : downloadingData.count - self.maxConcurrentCount;
  211. for (NSInteger i = 0; i < count; i++) {
  212. // 取消任务
  213. RQDownloadModel *model = downloadingData[i];
  214. [self cancelTaskWithModel:model delete:NO];
  215. // 更新状态为等待
  216. model.status = RQDownloadStatus_Waiting;
  217. [[RQDataBaseManager shareManager] updateWithModel:model option:RQDBUpdateOptionAllParam];
  218. }
  219. }
  220. - (void)applicationWillTerminate {
  221. [self updateDownloadingTaskState];
  222. }
  223. #pragma mark - NSURLSessionDownloadDelegate
  224. // 接收到服务器返回数据,会被调用多次
  225. - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
  226. {
  227. // 获取模型
  228. RQDownloadModel *model = [[RQDataBaseManager shareManager] getModelWithUrl:downloadTask.taskDescription];
  229. // 更新当前下载大小
  230. model.fileDownloadSize = totalBytesWritten;
  231. model.fileTotalSize = totalBytesExpectedToWrite;
  232. // 计算速度时间内下载文件的大小
  233. model.intervalFileSize += bytesWritten;
  234. // 获取上次计算时间与当前时间间隔
  235. NSInteger intervals = [RQ_SHARE_FUNCTION getIntervalsWithTimeStamp:model.lastSpeedTime];
  236. if (intervals >= 1) {
  237. // 计算速度
  238. model.speed = model.intervalFileSize / intervals;
  239. // 重置变量
  240. model.intervalFileSize = 0;
  241. model.lastSpeedTime = [RQ_SHARE_FUNCTION getTimeStampWithDate:[NSDate date]];
  242. }
  243. // 计算进度
  244. double byts = totalBytesWritten * 1.0 / 1024 /1024;
  245. double total = totalBytesExpectedToWrite * 1.0 / 1024 /1024;
  246. NSString *text = [NSString stringWithFormat:@"%.1lfMB/%.1lfMB",byts,total];
  247. CGFloat progress = 1.0 * byts / total;
  248. model.statusText = text;
  249. model.progress = progress;
  250. NSLog(@"%@",text);
  251. // 更新数据库中数据
  252. [[RQDataBaseManager shareManager] updateWithModel:model option:RQDBUpdateOptionAllParam];
  253. // 进度通知
  254. [[NSNotificationCenter defaultCenter] postNotificationName:RQDownloadProgressNotification object:model];
  255. }
  256. // 下载完成
  257. - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
  258. {
  259. // 获取模型
  260. RQDownloadModel *model = [[RQDataBaseManager shareManager] getModelWithUrl:downloadTask.taskDescription];
  261. // 移动文件,原路径文件由系统自动删除
  262. NSError *error = nil;
  263. if (model) {
  264. [[NSFileManager defaultManager] moveItemAtPath:[location path] toPath:model.destinationPath error:&error];
  265. if (error) NSLog(@"下载完成,移动文件发生错误:%@", error);
  266. }
  267. }
  268. #pragma mark - NSURLSessionTaskDelegate
  269. // 请求完成
  270. - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
  271. {
  272. // 调用cancel方法直接返回,在相应操作是直接进行处理
  273. if (error && [error.localizedDescription isEqualToString:@"cancelled"]) return;
  274. // 获取模型
  275. RQDownloadModel *model = [[RQDataBaseManager shareManager] getModelWithUrl:task.taskDescription];
  276. // 下载时,进程杀死,重新启动,回调错误
  277. if (error && [error.userInfo objectForKey:NSURLErrorBackgroundTaskCancelledReasonKey]) {
  278. model.status = RQDownloadStatus_Waiting;
  279. model.resumeData = [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData];
  280. [[RQDataBaseManager shareManager] updateWithModel:model option:RQDBUpdateOptionAllParam];
  281. return;
  282. }
  283. // 更新下载数据、任务状态
  284. if (error) {
  285. model.status = RQDownloadStatus_Failed;
  286. model.resumeData = [error.userInfo objectForKey:NSURLSessionDownloadTaskResumeData];
  287. [[RQDataBaseManager shareManager] updateWithModel:model option:RQDBUpdateOptionAllParam];
  288. }else {
  289. model.status = RQDownloadStatus_Completed;
  290. }
  291. // 更新数据
  292. if (_currentCount > 0) _currentCount--;
  293. if (model.urlString) {
  294. [_dataTaskDic removeObjectForKey:model.urlString];
  295. }
  296. // 更新数据库状态
  297. [[RQDataBaseManager shareManager] updateWithModel:model option:RQDBUpdateOptionAllParam];
  298. // 开启等待下载任务
  299. [self startDownloadWaitingTask];
  300. NSLog(@"\n 文件:%@,下载完成 \n 本地路径:%@ \n 错误:%@ \n", model.fileName, model.destinationPath, error);
  301. }
  302. #pragma mark - NSURLSessionDelegate
  303. // 应用处于后台,所有下载任务完成及NSURLSession协议调用之后调用
  304. - (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
  305. {
  306. dispatch_async(dispatch_get_main_queue(), ^{
  307. AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
  308. if (appDelegate.backgroundSessionCompletionHandler) {
  309. void (^completionHandler)(void) = appDelegate.backgroundSessionCompletionHandler;
  310. appDelegate.backgroundSessionCompletionHandler = nil;
  311. // 执行block,系统后台生成快照,释放阻止应用挂起的断言
  312. completionHandler();
  313. }
  314. });
  315. }
  316. #pragma mark - RQDownloadMaxConcurrentCountChangeNotification
  317. - (void)downloadMaxConcurrentCountChange:(NSNotification *)notification {
  318. self.maxConcurrentCount = [notification.object integerValue];
  319. if (_currentCount < self.maxConcurrentCount) {
  320. // 当前下载数小于并发数,开启等待下载任务
  321. [self startDownloadWaitingTask];
  322. }else if (_currentCount > self.maxConcurrentCount) {
  323. // 变更正在下载任务为等待下载
  324. [self pauseDownloadingTaskWithAll:NO];
  325. }
  326. }
  327. #pragma mark - RQDownloadAllowsCellularAccessChangeNotification
  328. - (void)downloadAllowsCellularAccessChange:(NSNotification *)notification {
  329. _allowsCellularAccess = [notification.object boolValue];
  330. [self allowsCellularAccessOrNetworkingReachabilityDidChangeAction];
  331. }
  332. #pragma mark - RQNetworkingReachabilityDidChangeNotification
  333. - (void)networkingReachabilityDidChange:(NSNotification *)notification
  334. {
  335. [self allowsCellularAccessOrNetworkingReachabilityDidChangeAction];
  336. }
  337. // 是否允许蜂窝网络下载或网络状态变更事件
  338. - (void)allowsCellularAccessOrNetworkingReachabilityDidChangeAction {
  339. if ([[RQNetworkReachabilityManager shareManager] networkReachabilityStatus] == AFNetworkReachabilityStatusNotReachable) {
  340. // 无网络,暂停正在下载任务
  341. [self pauseDownloadingTaskWithAll:YES];
  342. }else {
  343. if ([self networkingAllowsDownloadTask]) {
  344. // 开启等待任务
  345. [self startDownloadWaitingTask];
  346. }else {
  347. // 增加一个友善的提示,蜂窝网络情况下如果有正在下载,提示已暂停
  348. if ([kRQDownloadDataBaseManager getLastDownloadingModel]) {
  349. ShowMsg(@"当前为蜂窝网络,已停止下载任务,可在设置中开启");
  350. }
  351. // 当前为蜂窝网络,不允许下载,暂停正在下载任务
  352. [self pauseDownloadingTaskWithAll:YES];
  353. }
  354. }
  355. }
  356. // 是否允许下载任务
  357. - (BOOL)networkingAllowsDownloadTask {
  358. // 当前网络状态
  359. AFNetworkReachabilityStatus status = [[RQNetworkReachabilityManager shareManager] networkReachabilityStatus];
  360. // 无网络 或 (当前为蜂窝网络,且不允许蜂窝网络下载)
  361. // if (status == AFNetworkReachabilityStatusNotReachable || (status == AFNetworkReachabilityStatusReachableViaWWAN && !_allowsCellularAccess)) {
  362. // return NO;
  363. // }
  364. if (status == AFNetworkReachabilityStatusNotReachable) {
  365. return NO;
  366. }
  367. return YES;
  368. }
  369. - (void)dealloc {
  370. [_session invalidateAndCancel];
  371. [[NSNotificationCenter defaultCenter] removeObserver:self];
  372. }
  373. #pragma mark - LazyLoad
  374. @end