【iOS】文件下载管理器(上)

序言

在做项目的时候经常会用到单文件下载或者批量文件的下载,并且需要实现断点续传,状态变更,进度回调等逻辑。本篇为上文,先实现单文件的下载与状态变更。若需要批量下载,只需要在此基础上创建多个下载器关联即可,下篇文章会详细再写到。

为何选用NSURLSession?

我们选择使用NSURLSession实现完成下载。

原因:苹果已经放弃使用NSURLConnection,而推出更强大的NSURLSession,目前支持最低 iOS 7.0 系统,便于以后拓展维护。

接下来我们开始进入正题。

实现个单文件下载器

  1. 我们首先创建NSObject的子类ZXDownLoader.h,并且自定义一个较为便捷的方法,调用此方法开启下载。
  • ZXDownLoader.h中
- (void)zx_downLoadWithURL:(NSURL *)URL downloadStateChange:(ZXDownLoadStateChangeBlock)stateChange progressBlock:(ZXProgressCompleBlock)progress successed:(ZXDownFinishedBlock)success failed:(ZXDownFailedBlock)failed;

下载过程中的,文件的的状态,进度,结果等我们选择使用block进行回调。当然,也可以选择代理或者通知,看自己喜好。

typedef enum : NSUInteger {
    ZXDownloadStateNormal = 1,
    ZXDownloadStatePause = 2, //暂停
    ZXDownloadStateDowning, //下载中
    ZXDownloadStateSuccess, //成功
    ZXDownloadStateFailed //失败
} ZXDownloadStateState;

typedef enum : NSUInteger {
    ZXDownFailedCodeNetError = 0,
} ZXDownFailedErrorCode;

typedef void(^ZXProgressCompleBlock)(long long totalSize,  long long currentSize, CGFloat progress, CGFloat speed);
typedef void(^ZXDownLoadStateChangeBlock)(ZXDownloadStateState state);
typedef void(^ZXDownFinishedBlock)(NSString *downFinishedPath);
typedef void(^ZXDownFailedBlock)(ZXDownFailedErrorCode code);

下载数据初始化,详细步骤可以看以下代码注释

  • ZXDownLoader.m中
- (void)zx_downLoadWithURL:(NSURL *)URL downloadStateChange:(ZXDownLoadStateChangeBlock)stateChange progressBlock:(ZXProgressCompleBlock)progress successed:(ZXDownFinishedBlock)success failed:(ZXDownFailedBlock)failed{
    self.progressCompleteBlock = progress;
    self.downLoadStateChangeBlock = stateChange;
    self.downFailedBlock = failed;
    self.downFinishedBlock = success;
    
    [self zx_downLoadWithURL:URL];
}

- (void)zx_downLoadWithURL:(NSURL *)URL{
     //安全判断
    if (URL.absoluteString.length ==0) {
        return;
    }
    
    //文件临时路径与最终存储的路径
    self.tempDownPath = tmpPath(URL.lastPathComponent);
    self.finishDownPath = dscPath(URL.lastPathComponent);
    
    //判断该文件是否已经下载完成,若下载完成,直接return
    if ([ZXFileTools fileExist:self.finishDownPath]) {
        self.task_state = ZXDownloadStateSuccess;
        return;
    }
    
    //判断该文件是否已经正在执行中,若文件状态为暂停 ,则继续下载,若正在下载,则return
    if ([URL isEqual:self.dataTask.originalRequest.URL]) {
        
        if (self.task_state == ZXDownloadStateDowning) {
            return;
        }
       
        if(self.task_state == ZXDownloadStatePause){
            [self zx_resumeCurrentTask];
            return;
        }
    }
     //其它情况,最好取消之前的任务,再取出临时路中的文件,以实现断点续传
    [self zx_cancelTask];
    
    if ([ZXFileTools fileExist:self.tempDownPath]) {
        self.downLoadedFileSize = [ZXFileTools fileSize:self.tempDownPath];
    }
    
    [self downLoadURL:URL Offset:self.downLoadedFileSize];
}
- (void)downLoadURL:(NSURL *)URL Offset:(long long)offSet{
    self.task_URL = URL;
   // 表示请求不超时
    NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:URL cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:0];
    // 设置请求求信息,然后开启下载任务
    [request setValue:[NSString stringWithFormat:@"byte=%lld-",self.downLoadedFileSize] forHTTPHeaderField:@"Range"];
    self.dataTask = [self.session dataTaskWithRequest:request];
    [self zx_resumeCurrentTask];
}
  1. 根据NSURLSession的代理回调,进行文件下载器属性的更改,状态变化 ,数据的计算,存储与本地化。
  • 文件真正下载开始之前,此代理会在接收数据之前优先做出响应,末尾的block需要我们自己主动传参,告诉session是否允许开始下载。
    注意,在这个代理中,我们可以拿到很多关于下载文件的信息
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSHTTPURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler{
    self.totalFileSize =  response.expectedContentLength + self.downLoadedFileSize;
    //如若文件与实际文件大小不符,直接删除本地缓存,重新执行下载。
    if (self.downLoadedFileSize > self.totalFileSize) {
        [ZXFileTools removeFile:self.tempDownPath];
        completionHandler(NSURLSessionResponseCancel);
        [self zx_downLoadWithURL:response.URL];
        return;
    }
    
    if (self.downLoadedFileSize == self.totalFileSize) {
        [ZXFileTools moveFileFromPath:self.tempDownPath toPath:self.finishDownPath];
         completionHandler(NSURLSessionResponseCancel);
         self.task_state = ZXDownloadStateSuccess;
        return;
    }
    
    completionHandler(NSURLSessionResponseAllow);
    
    //这里可以初始化输出流,将文件写入本地指定的目录中,并更改文件的下载状态
    self.outputStream = [NSOutputStream outputStreamToFileAtPath:self.tempDownPath append:YES];
    [self.outputStream open];
    self.task_state = ZXDownloadStateDowning;
}

  • 文件开启下载,此方法在下载过程会多次回调。
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data{
    
    self.downLoadedFileSize += data.length;
    //下载器速度计算
    [self caculatorDownloadSpeed:data.length];
    //进度回调计算
    [self caculatorDownloadProgress];
    //数据本地化写入
    [self.outputStream write:data.bytes maxLength:data.length];
}

- (void)caculatorDownloadSpeed:(long long)dataLenth{
    _timeDurationTotalDownFileSize += dataLenth;
    if (_lastDate == nil) {
        _lastDate = [NSDate date];
    }else{
        NSDate *currentDate = [NSDate date];
        NSTimeInterval  timeDuration = [currentDate timeIntervalSinceDate:_lastDate];
        if(timeDuration >= kDownloadingSpeedDuration){
            self.task_downloading_speed = 1.0 * _timeDurationTotalDownFileSize / kDownloadingSpeedDuration;
            _timeDurationTotalDownFileSize = 0;
            _lastDate = currentDate;
        }
    }
}

- (void)caculatorDownloadProgress{
     self.task_progress_value = 1.0 * self.downLoadedFileSize / self.totalFileSize;
}

  • 下载任务完成,或者异常状态会调用此代理。注意:此方法被调用,并不一定表示下载完成。只是表示此次下载任务终结,比如断网,或者暂停,或者下载完成,或者下载失败等,我们可以根据状态error码来进行具体的判断
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error{
 //error为空,且下载长度等同于期望的总长度表示下载完成(严格意义上是不准确的,如果有条件,我们可以凭借文件的md5等来判定。但是我们这里当前只凭借文件长度来判断文件的完整性,)
    if (error == nil) {
        [ZXFileTools moveFileFromPath:self.tempDownPath toPath:self.finishDownPath];
         self.task_state = ZXDownloadStateSuccess;
    }else{
        if (error.code == -999) {
             self.task_state = ZXDownloadStateNormal;
        }else{
            
            self.task_state = ZXDownloadStateFailed;
            NSLog(@"失败了----%@---%ld",error.localizedDescription,error.code);
            if (self.downFailedBlock) {
                self.downFailedBlock(ZXDownFailedCodeNetError);
            }
        }
    }
    //下载完成,关闭输出流。下次下载,上面的下载开始前会再次打开,所以不用担心。
    [self.outputStream close];
    self.outputStream = nil;
}

  1. 下载的结果,状态,我们统一在进度,状态的set方法里进行blockr的回调处理。
- (void)setTask_progress_value:(CGFloat)task_progress_value{
    _task_progress_value = task_progress_value;
    
    if (self.progressCompleteBlock) {
        self.progressCompleteBlock(self.totalFileSize,self.downLoadedFileSize,task_progress_value,self.task_downloading_speed);
    }
}

- (void)setTask_state:(ZXDownloadStateState)task_state{
    if (_task_state == task_state) {
        return;
    }
    _task_state = task_state;

    if (self.downLoadStateChangeBlock) {
        self.downLoadStateChangeBlock(task_state);
    }
    
    if (task_state == ZXDownloadStateSuccess) {
        if (self.downFinishedBlock) {
            self.downFinishedBlock(self.finishDownPath);
        }
    }
}

经过以上三步,下载器就算是已经基本完成了。使用的时候,我们直接传入对应的参数就可以直接进行下载了,并完成相应的参数回调。

结束语

本篇只是简单的一个文件下载器,下篇我们以此为基础实现多文件批量下载。完整源码

你可能感兴趣的:(【iOS】文件下载管理器(上))