我正在参加「掘金·启航计划」
前言
项目自从接入 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
先进行 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;
}
接着我们编写一个下载的方法,记住的是每次获取字典对应的下载对象DownloadObject,添加修改删除等必须添加锁。
这里的一个重点是,如果 urlString 就在我们字典当中,需要覆盖之前的,包括优先级。
- (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];
}
第二个就是下载成功回调。这时候我们首先需要保证我们的目录是存在的,没有就创建一个。
重点是把下载的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: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];
}
}
至此,我们下载的实现就基本完成了。我们还需要做的,就是加入取消单个下载方法,暂停单个下载的方法,继续下载单个的下载方法,这些都可以基于上述2个队列进行调整。上述代码有些还不够完整,但对于开发者来说问题不大。
最后
下载文件管理来说,上述的方案其实也是基于现有业务进行调整的,之前是一个下载的队列,现在拆分出2个,然后加了一个自定义的优先级priority,和系统的类似,我们去等待队列中拿数据,也是取出当前优先级最高的,然后添加到下载队列当中。其实也不能说是队列,就是2个字典,说的好听一点而已。对此你觉得有什么更好的方案去做呢,欢迎留意。