我正在参加「掘金·启航计划」
前言
项目自从接入 unity 后,关于资源方面下载数据就增多了,下载种类也变的不一样了。以前手动下载,直接调一下封装好的的 API 就好,也没什么难度,但现在又说什么静默下载,预下载,手动下载,WIFI时候下载,4G网络不下载等等。说白了就是需要做一个下载的优先级管理了。
思路
我们做一个下载,必然有开始,暂停,取消,继续下载,重复下载等等情况,如果有大量的 URL 在同时下载,然后 URL 的状态可能会随时发生变化,那怎么做会比较好控制状态呢。这里提供一个思路,首先弄一个下载队列就设置最大下载数为3个,也就是说并发下载就只有3个,剩下的都放在等待队列当中。一旦有下载完成的,或者失败的,就移除当前的,去等待队列当中随机取出第一个,放到下载队列当中。这样的好处是什么呢,就是我下载的线程永远最多只有3个,这样很容易方便我们去维护状态,这就是我个人思路。
下载管理
现在我们就按照上面的思路去写,首先我们创建一个下载管理的单例 DownloadManager,先添加一个 NSURLSession
。
@interface DownloadManager () <NSURLSessionDelegate, NSURLSessionDownloadDelegate>@property (nonatomic, strong) NSURLSession *session;// 锁@property (nonatomic, strong) NSLock *downloadsLock;// 下载中的队列@property (nonatomic, strong) NSMutableDictionary *downloads;// 等待中的队列@property (nonatomic, strong) NSMutableDictionary *waitDownloads;// 最大下载数据@property (nonatomic, assign) NSInteger downloadMaxCount;@end@interface DownloadManager () <NSURLSessionDelegate, NSURLSessionDownloadDelegate> @property (nonatomic, strong) NSURLSession *session; // 锁 @property (nonatomic, strong) NSLock *downloadsLock; // 下载中的队列 @property (nonatomic, strong) NSMutableDictionary *downloads; // 等待中的队列 @property (nonatomic, strong) NSMutableDictionary *waitDownloads; // 最大下载数据 @property (nonatomic, assign) NSInteger downloadMaxCount; @end@interface DownloadManager () <NSURLSessionDelegate, NSURLSessionDownloadDelegate> @property (nonatomic, strong) NSURLSession *session; // 锁 @property (nonatomic, strong) NSLock *downloadsLock; // 下载中的队列 @property (nonatomic, strong) NSMutableDictionary *downloads; // 等待中的队列 @property (nonatomic, strong) NSMutableDictionary *waitDownloads; // 最大下载数据 @property (nonatomic, assign) NSInteger downloadMaxCount; @end
先进行 NSURLSession
初始化,然后添加2个字典,1个为下载中的字典,1个为等待中的字典。
- (instancetype)init{self = [super init];if (self){NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil];self.downloadsLock = [[NSLock alloc] init];self.downloads = [NSMutableDictionary new];self.waitDownloads = [NSMutableDictionary new];self.downloadMaxCount = 3;}return self;}- (instancetype)init { self = [super init]; if (self) { NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil]; self.downloadsLock = [[NSLock alloc] init]; self.downloads = [NSMutableDictionary new]; self.waitDownloads = [NSMutableDictionary new]; self.downloadMaxCount = 3; } return self; }- (instancetype)init { self = [super init]; if (self) { NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration]; self.session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:nil]; self.downloadsLock = [[NSLock alloc] init]; self.downloads = [NSMutableDictionary new]; self.waitDownloads = [NSMutableDictionary new]; self.downloadMaxCount = 3; } return self; }
接着我们编写一个下载的方法,记住的是每次获取字典对应的下载对象DownloadObject
,添加修改删除等必须添加锁。
这里的一个重点是,如果 urlString
就在我们字典当中,需要覆盖之前的,包括优先级。
- (void)downloadFileForURL:(NSString *)urlStringfileName:(NSString *)fileNamedirectory:(NSString *)directorypriority:(OPRDownloadPriority)priorityprogressBlock:(void(^)(CGFloat progress))progressBlockcompletionBlock:(void(^)(BOOL completed, NSInteger code))completionBlock {// 资源本来就在本地if ([self fileExistsWithName:fileName inDirectory:directory]){completionBlock(YES,0);return;}{[self.downloadsLock lock];// 所有下载队列NSMutableDictionary *allDownloads = [self allDownloads];DownloadObject *download = [allDownloads objectForKey:urlString];[self.downloadsLock unlock];if (download){// 本来就在队列中download.completionBlock = completionBlock;download.progressBlock = progressBlock;download.priority = priority;return;}}NSURLRequest *request = [NSURLRequest requestWithURL:url];NSURLSessionDownloadTask *downloadTask = [self.session downloadTaskWithRequest:request];DownloadObject *downloadObject = [[DownloadObject alloc] initWithDownloadTask:downloadTaskprogressBlock:progressBlockcompletionBlock:completionBlock];downloadObject.fileName = fileName;downloadObject.directoryName = directory;downloadObject.priority = priority;[self.downloadsLock lock];//下载队列少于最大下载数量3,则把URLString 添加到队列中,否则放到等待队列if (self.downloads.count < self.downloadMaxCount) {[self.downloads addEntriesFromDictionary:@{urlString:downloadObject}];[downloadTask resume];}else {[self.waitDownloads addEntriesFromDictionary:@{urlString:downloadObject}];}[self.downloadsLock unlock];}- (void)downloadFileForURL:(NSString *)urlString fileName:(NSString *)fileName directory:(NSString *)directory priority:(OPRDownloadPriority)priority progressBlock:(void(^)(CGFloat progress))progressBlock completionBlock:(void(^)(BOOL completed, NSInteger code))completionBlock { // 资源本来就在本地 if ([self fileExistsWithName:fileName inDirectory:directory]) { completionBlock(YES,0); return; } { [self.downloadsLock lock]; // 所有下载队列 NSMutableDictionary *allDownloads = [self allDownloads]; DownloadObject *download = [allDownloads objectForKey:urlString]; [self.downloadsLock unlock]; if (download) { // 本来就在队列中 download.completionBlock = completionBlock; download.progressBlock = progressBlock; download.priority = priority; return; } } NSURLRequest *request = [NSURLRequest requestWithURL:url]; NSURLSessionDownloadTask *downloadTask = [self.session downloadTaskWithRequest:request]; DownloadObject *downloadObject = [[DownloadObject alloc] initWithDownloadTask:downloadTask progressBlock:progressBlock completionBlock:completionBlock]; downloadObject.fileName = fileName; downloadObject.directoryName = directory; downloadObject.priority = priority; [self.downloadsLock lock]; //下载队列少于最大下载数量3,则把URLString 添加到队列中,否则放到等待队列 if (self.downloads.count < self.downloadMaxCount) { [self.downloads addEntriesFromDictionary:@{urlString:downloadObject}]; [downloadTask resume]; }else { [self.waitDownloads addEntriesFromDictionary:@{urlString:downloadObject}]; } [self.downloadsLock unlock]; }- (void)downloadFileForURL:(NSString *)urlString fileName:(NSString *)fileName directory:(NSString *)directory priority:(OPRDownloadPriority)priority progressBlock:(void(^)(CGFloat progress))progressBlock completionBlock:(void(^)(BOOL completed, NSInteger code))completionBlock { // 资源本来就在本地 if ([self fileExistsWithName:fileName inDirectory:directory]) { completionBlock(YES,0); return; } { [self.downloadsLock lock]; // 所有下载队列 NSMutableDictionary *allDownloads = [self allDownloads]; DownloadObject *download = [allDownloads objectForKey:urlString]; [self.downloadsLock unlock]; if (download) { // 本来就在队列中 download.completionBlock = completionBlock; download.progressBlock = progressBlock; download.priority = priority; return; } } NSURLRequest *request = [NSURLRequest requestWithURL:url]; NSURLSessionDownloadTask *downloadTask = [self.session downloadTaskWithRequest:request]; DownloadObject *downloadObject = [[DownloadObject alloc] initWithDownloadTask:downloadTask progressBlock:progressBlock completionBlock:completionBlock]; downloadObject.fileName = fileName; downloadObject.directoryName = directory; downloadObject.priority = priority; [self.downloadsLock lock]; //下载队列少于最大下载数量3,则把URLString 添加到队列中,否则放到等待队列 if (self.downloads.count < self.downloadMaxCount) { [self.downloads addEntriesFromDictionary:@{urlString:downloadObject}]; [downloadTask resume]; }else { [self.waitDownloads addEntriesFromDictionary:@{urlString:downloadObject}]; } [self.downloadsLock unlock]; }
我们 NSURLSession
会监听下载状态,第一个是接受服务端返回的数据。这里我们主要是用来监听 URLString
的下载进度,我们DownloadObject
里面就保存了progressBlock
用来返回进度。
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{NSString *fileIdentifier = downloadTask.originalRequest.URL.absoluteString;if (!fileIdentifier){return;}[self.downloadsLock lock];DownloadObject *download = [self.downloads objectForKey:fileIdentifier];if (download.progressBlock){CGFloat progress = (CGFloat)totalBytesWritten / (CGFloat)totalBytesExpectedToWrite;dispatch_async(dispatch_get_main_queue(), ^(void) {download.progressBlock(progress);});}[self.downloadsLock unlock];}- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { NSString *fileIdentifier = downloadTask.originalRequest.URL.absoluteString; if (!fileIdentifier) { return; } [self.downloadsLock lock]; DownloadObject *download = [self.downloads objectForKey:fileIdentifier]; if (download.progressBlock) { CGFloat progress = (CGFloat)totalBytesWritten / (CGFloat)totalBytesExpectedToWrite; dispatch_async(dispatch_get_main_queue(), ^(void) { download.progressBlock(progress); }); } [self.downloadsLock unlock]; }- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite { NSString *fileIdentifier = downloadTask.originalRequest.URL.absoluteString; if (!fileIdentifier) { return; } [self.downloadsLock lock]; DownloadObject *download = [self.downloads objectForKey:fileIdentifier]; if (download.progressBlock) { CGFloat progress = (CGFloat)totalBytesWritten / (CGFloat)totalBytesExpectedToWrite; dispatch_async(dispatch_get_main_queue(), ^(void) { download.progressBlock(progress); }); } [self.downloadsLock unlock]; }
第二个就是下载成功回调。这时候我们首先需要保证我们的目录是存在的,没有就创建一个。
重点是把下载的location
,移动到我们的directoryName
当中。并且最后需要把 URLString
移出下载队列,然后我们再去等待队列中,看是否有再等待的数据,有就添加到下载队列继续下载。
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location{NSString *fileIdentifier = downloadTask.originalRequest.URL.absoluteString;NSURL *destinationLocation;// 如果目录没有创建,就创建一个目录if (download.directoryName&& [self createDirectoryNamed:download.directoryName]){destinationLocation = [[[self cachesDirectoryUrlPath] URLByAppendingPathComponent:download.directoryName] URLByAppendingPathComponent:download.fileName];}// 把下载的TMP目录移动到我们下载需要保存的目录[[NSFileManager defaultManager] moveItemAtURL:locationtoURL:destinationLocationerror:&error];if (download.completionBlock){dispatch_async(dispatch_get_main_queue(), ^(void) {download.completionBlock(success,200);});}[self.downloads removeObjectForKey:fileIdentifier];[self addWaitQueueToDownload];[self.downloadsLock unlock];}- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { NSString *fileIdentifier = downloadTask.originalRequest.URL.absoluteString; NSURL *destinationLocation; // 如果目录没有创建,就创建一个目录 if (download.directoryName && [self createDirectoryNamed:download.directoryName]) { destinationLocation = [[[self cachesDirectoryUrlPath] URLByAppendingPathComponent:download.directoryName] URLByAppendingPathComponent:download.fileName]; } // 把下载的TMP目录移动到我们下载需要保存的目录 [[NSFileManager defaultManager] moveItemAtURL:location toURL:destinationLocation error:&error]; if (download.completionBlock) { dispatch_async(dispatch_get_main_queue(), ^(void) { download.completionBlock(success,200); }); } [self.downloads removeObjectForKey:fileIdentifier]; [self addWaitQueueToDownload]; [self.downloadsLock unlock]; }- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location { NSString *fileIdentifier = downloadTask.originalRequest.URL.absoluteString; NSURL *destinationLocation; // 如果目录没有创建,就创建一个目录 if (download.directoryName && [self createDirectoryNamed:download.directoryName]) { destinationLocation = [[[self cachesDirectoryUrlPath] URLByAppendingPathComponent:download.directoryName] URLByAppendingPathComponent:download.fileName]; } // 把下载的TMP目录移动到我们下载需要保存的目录 [[NSFileManager defaultManager] moveItemAtURL:location toURL:destinationLocation error:&error]; if (download.completionBlock) { dispatch_async(dispatch_get_main_queue(), ^(void) { download.completionBlock(success,200); }); } [self.downloads removeObjectForKey:fileIdentifier]; [self addWaitQueueToDownload]; [self.downloadsLock unlock]; }
最后一个是下载错误。不管是超时还是其它原因,这里都给多一次重试的机会,最后也是通过completionBlock
回调给下载的方法,然后我们再去等待队列中,看是否有再等待的数据,有就添加到下载队列继续下载,这里和下载完成逻辑是一样的。
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{if (error){NSString *fileIdentifier = task.originalRequest.URL.absoluteString;BOOL retry = NO;if(error.code == NSURLErrorTimedOut){retry = YES;}[self.downloadsLock lock];DownloadObject *download = [self.downloads objectForKey:fileIdentifier];if(!retry && !download.hasRetry){retry = YES;download.hasRetry = YES;}if(retry){// 重试下载NSURLRequest *request = [NSURLRequest requestWithURL:task.currentRequest.URL];NSURLSessionDownloadTask *downloadTask = [self.session downloadTaskWithRequest:request];download.downloadTask = downloadTask;[downloadTask resume];}else{if (download.completionBlock){dispatch_async(dispatch_get_main_queue(), ^(void) {download.completionBlock(NO,error.code);});}[self.downloads removeObjectForKey:fileIdentifier];[self addWaitQueueToDownload];}[self.downloadsLock unlock];}}- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { if (error) { NSString *fileIdentifier = task.originalRequest.URL.absoluteString; BOOL retry = NO; if(error.code == NSURLErrorTimedOut) { retry = YES; } [self.downloadsLock lock]; DownloadObject *download = [self.downloads objectForKey:fileIdentifier]; if(!retry && !download.hasRetry) { retry = YES; download.hasRetry = YES; } if(retry) { // 重试下载 NSURLRequest *request = [NSURLRequest requestWithURL:task.currentRequest.URL]; NSURLSessionDownloadTask *downloadTask = [self.session downloadTaskWithRequest:request]; download.downloadTask = downloadTask; [downloadTask resume]; } else { if (download.completionBlock) { dispatch_async(dispatch_get_main_queue(), ^(void) { download.completionBlock(NO,error.code); }); } [self.downloads removeObjectForKey:fileIdentifier]; [self addWaitQueueToDownload]; } [self.downloadsLock unlock]; } }- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { if (error) { NSString *fileIdentifier = task.originalRequest.URL.absoluteString; BOOL retry = NO; if(error.code == NSURLErrorTimedOut) { retry = YES; } [self.downloadsLock lock]; DownloadObject *download = [self.downloads objectForKey:fileIdentifier]; if(!retry && !download.hasRetry) { retry = YES; download.hasRetry = YES; } if(retry) { // 重试下载 NSURLRequest *request = [NSURLRequest requestWithURL:task.currentRequest.URL]; NSURLSessionDownloadTask *downloadTask = [self.session downloadTaskWithRequest:request]; download.downloadTask = downloadTask; [downloadTask resume]; } else { if (download.completionBlock) { dispatch_async(dispatch_get_main_queue(), ^(void) { download.completionBlock(NO,error.code); }); } [self.downloads removeObjectForKey:fileIdentifier]; [self addWaitQueueToDownload]; } [self.downloadsLock unlock]; } }
至此,我们下载的实现就基本完成了。我们还需要做的,就是加入取消单个下载方法,暂停单个下载的方法,继续下载单个的下载方法,这些都可以基于上述2个队列进行调整。上述代码有些还不够完整,但对于开发者来说问题不大。
最后
下载文件管理来说,上述的方案其实也是基于现有业务进行调整的,之前是一个下载的队列,现在拆分出2个,然后加了一个自定义的优先级priority
,和系统的类似,我们去等待队列中拿数据,也是取出当前优先级最高的,然后添加到下载队列当中。其实也不能说是队列,就是2个字典,说的好听一点而已。对此你觉得有什么更好的方案去做呢,欢迎留意。