我正在参加「启航方案」

前语

项目自从接入 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个字典,说的好听一点罢了。对此你觉得有什么更好的方案去做呢,欢迎留意。