iOS 下载多文件管理

我正在参加「掘金·启航计划」

前言

项目自从接入 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 *)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];
}
- (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: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];
  
}
- (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个字典,说的好听一点而已。对此你觉得有什么更好的方案去做呢,欢迎留意。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYHgP9Ng' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片