序言
在做项目的时候经常会用到单文件下载或者批量文件的下载,并且需要实现断点续传,状态变更,进度回调等逻辑。本篇为上文,先实现单文件的下载与状态变更。若需要批量下载,只需要在此基础上创建多个下载器关联即可,下篇文章会详细再写到。
为何选用NSURLSession?
我们选择使用NSURLSession实现完成下载。
原因:苹果已经放弃使用NSURLConnection,而推出更强大的NSURLSession,目前支持最低 iOS 7.0 系统,便于以后拓展维护。
接下来我们开始进入正题。
实现个单文件下载器
- 我们首先创建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];
}
- 根据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;
}
- 下载的结果,状态,我们统一在进度,状态的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);
}
}
}
经过以上三步,下载器就算是已经基本完成了。使用的时候,我们直接传入对应的参数就可以直接进行下载了,并完成相应的参数回调。
结束语
本篇只是简单的一个文件下载器,下篇我们以此为基础实现多文件批量下载。完整源码。