【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);
        }
    }
}

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

结束语

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,525评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,203评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,862评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,728评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,743评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,590评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,330评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,244评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,693评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,885评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,001评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,723评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,343评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,919评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,042评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,191评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,955评论 2 355

推荐阅读更多精彩内容