美文网首页iOS
iOS开发之多任务文件下载,断点续传

iOS开发之多任务文件下载,断点续传

作者: charlotte2018 | 来源:发表于2017-03-31 10:46 被阅读864次

    NSURLSession实现下载有两种方式,一种是通过NSURLSessionDataTask去实现,但是这个对象实现的下载是不支持后台下载的,但是他的断点续传是支持的很好。要想实现后台下载就必须使用NSURLSessionDownloadTask这个对象,今天先研究NSURLSessionDataTask的这个适合小文件的,在前台不用等几秒就能下载好的,比如下载个音乐文件,几兆的。如果要是做腾讯视频的大视频下载,得做后台下载。用户不会等很久的,这个需求是必须的。等有时间研究下NSURLSessionDownloadTask的。

    要实现断点续传主要是把第一次下载的文件的大小记录下来。下次在请求头上告诉服务器从上次停止的地方开始下载。

    20150908155815582.png

    还有个注意点就是下载的文件都放在内存会爆的,所以下载一点存储一点。

    上代码前先写写用的类。YBFileModel是存储文件的信息,也是一个下载的任务。YBDownloadManager是个单例,管理YBFileModel,YBFileModel可以是多个。每个YBFileModel表示一个下载任务。支持下载的数量控制maxDownloadingCount。

    //
    //  YBFileModel.h
    //  YBDownLoadDemo
    //
    //  Created by wyb on 2017/4/5.
    //  Copyright © 2017年 中天易观. All rights reserved.
    //
    
    #import <Foundation/Foundation.h>
    
    /** 下载的状态 */
    typedef NS_ENUM(NSInteger, DownloadState) {
        DownloadStateNone = 0,     // 还没下载
        DownloadStateResumed,      // 下载中
        DownloadStateWait,      // 等待中
        DownloadStateStoped,     // 暂停中
        DownloadStateCompleted,     // 已经完全下载完毕
        DownloadStateError     // 下载出错
    };
    
    
    /**
     下载进度的回调
    
     @param thisTimeWrittenSize 这次回调返回的数据大小
     @param totlalReceivedSize 已经下载了的文件的大小
     @param TotalExpectedSize 总共期望下载文件的大小
     */
    typedef void (^ProgressBlock)(NSInteger thisTimeWrittenSize, NSInteger totlalReceivedSize, NSInteger TotalExpectedSize);
    
    /**
     *  状态改变的回调
     *
     *  @param filePath 文件的下载路径
     *  @param error    失败的描述信息
     */
    typedef void (^StateBlock)(DownloadState state, NSString *filePath, NSError *error);
    
    
    /**
     下载的文件信息
     */
    @interface YBFileModel : NSObject
    
    /** 下载状态 */
    @property (assign, nonatomic) DownloadState state;
    /** 文件名 */
    @property (copy, nonatomic) NSString *filename;
    /** 文件路径 */
    @property (copy, nonatomic) NSString *filePath;
    /** 文件url */
    @property (copy, nonatomic) NSString *fileUrl;
    /** 这次写入的数量 */
    @property (assign, nonatomic) NSInteger thisTimeWrittenSize;
    /** 已下载的数量 */
    @property (assign, nonatomic) NSInteger totlalReceivedSize;
    /** 文件的总大小 */
    @property (assign, nonatomic) NSInteger totalExpectedSize;
    /** 下载的错误信息 */
    @property (strong, nonatomic) NSError *error;
    /** 进度block */
    @property (copy, nonatomic) ProgressBlock progressBlock;
    /** 状态block */
    @property (copy, nonatomic) StateBlock stateBlock;
    /** 任务 */
    @property (strong, nonatomic) NSURLSessionDataTask *task;
    /** 文件流 */
    @property (strong, nonatomic) NSOutputStream *stream;
    
    - (void)setupTask:(NSURLSession *)session;
    
    /**
     *  恢复
     */
    - (void)resume;
    
    - (void)suspend;
    
    /**
     * 等待下载
     */
    - (void)waitDownload;
    
    - (void)didReceiveResponse:(NSHTTPURLResponse *)response;
    
    - (void)didReceiveData:(NSData *)data;
    
    - (void)didCompleteWithError:(NSError *)error;
    
    @end
    
    
    //
    //  YBFileModel.m
    //  YBDownLoadDemo
    //
    //  Created by wyb on 2017/4/5.
    //  Copyright © 2017年 中天易观. All rights reserved.
    //
    
    #import "YBFileModel.h"
    #import <CommonCrypto/CommonDigest.h>
    
    // 缓存主文件夹,所有下载下来的文件都放在这个文件夹下
    #define YBCacheDirectory [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"YBCacheDir"]
    
    
    @interface YBFileModel ()
    {
          DownloadState _state;
        
    }
    
    /** 存放所有的文件大小 */
    //@property(nonatomic,strong)NSMutableDictionary *totalFilesSizeDic;
    /** 存放所有的文件大小的文件路径 */
    @property(nonatomic,copy)NSString *totalFilesSizePath;
    
    @end
    
    @implementation YBFileModel
    
    - (NSString *)totalFilesSizePath
    {
        NSFileManager *manager = [NSFileManager defaultManager];
        BOOL isDic = false;
        BOOL isDirExist = [manager fileExistsAtPath:YBCacheDirectory isDirectory:&isDic];
        if (!isDic && !isDirExist) {
            
            //创建文件夹存放下载的文件
            [manager createDirectoryAtPath:YBCacheDirectory withIntermediateDirectories:YES attributes:nil error:nil];
        }
        
        //文件路径
        if (_totalFilesSizePath == nil) {
            _totalFilesSizePath = [YBCacheDirectory stringByAppendingPathComponent:@"downloadFileSizes.plist"];
        }
        
        return  _totalFilesSizePath;
    }
    
    - (NSString *)filePath
    {
        NSFileManager *manager = [NSFileManager defaultManager];
        BOOL isDic = false;
        BOOL isDirExist = [manager fileExistsAtPath:YBCacheDirectory isDirectory:&isDic];
        if (!isDic && !isDirExist) {
            
            //创建文件夹存放下载的文件
            [manager createDirectoryAtPath:YBCacheDirectory withIntermediateDirectories:YES attributes:nil error:nil];
        }
        
        //文件路径
        if (_filePath == nil) {
            _filePath = [YBCacheDirectory stringByAppendingPathComponent:self.filename];
        }
        
        return  _filePath;
        
    }
    
    - (NSString *)filename
    {
        if (_filename == nil) {
            //url的扩展名,如.mp4啥的
            NSString *fileExtension = self.fileUrl.pathExtension;
            NSString *fileNameMd5 = [self encryptFileNameWithMD5:self.fileUrl];
            if (fileExtension.length) {
                _filename = [NSString stringWithFormat:@"%@.%@", fileNameMd5, fileExtension];
            } else {
                _filename = fileNameMd5;
            }
        }
        return _filename;
    }
    
    
    /**
       将文件名md5加密
     */
    - (NSString *)encryptFileNameWithMD5:(NSString *)str
    {
        //要进行UTF8的转码
        const char* input = [str UTF8String];
        unsigned char result[CC_MD5_DIGEST_LENGTH];
        CC_MD5(input, (CC_LONG)strlen(input), result);
        
        NSMutableString *digest = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
        for (NSInteger i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
            [digest appendFormat:@"%02x", result[i]];
        }
        
        return digest;
    }
    
    - (NSOutputStream *)stream
    {
        if (_stream == nil) {
            _stream = [NSOutputStream outputStreamToFileAtPath:self.filePath append:YES];
        }
        return _stream;
    }
    
    - (NSInteger)totlalReceivedSize
    {
        NSFileManager *manager = [NSFileManager defaultManager];
        
        
        NSInteger reveiveSize = [[manager attributesOfItemAtPath:self.filePath error:nil][NSFileSize] integerValue];
        
        return reveiveSize;
    }
    
    - (NSInteger)totalExpectedSize
    {
        
        NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:self.totalFilesSizePath];
        if (dict == nil){
            _totalExpectedSize = 0;
        }else{
           
            _totalExpectedSize = [dict[self.fileUrl] integerValue];
            
        }
        
        return _totalExpectedSize;
    }
    
    
    
    - (void)setState:(DownloadState)state
    {
        DownloadState oldState = self.state;
        if (state == oldState) return;
        
        _state = state;
        
        // 发通知
        [self notifyStateChange];
    }
    
    - (void)notifyStateChange
    {
        if (self.stateBlock) {
            self.stateBlock(self.state, self.filePath, self.error);
        }
     
    }
    
    
    
    
    - (DownloadState)state
    {
        
        
        //下载完了
        if (self.totalExpectedSize && self.totalExpectedSize == self.totlalReceivedSize) {
            return DownloadStateCompleted;
        }
        
        //下载出错
        if (self.task.error) {
            
            return DownloadStateError;
        }
        
        return _state;
    }
    
    - (void)setupTask:(NSURLSession *)session
    {
        if (self.task) return;
        
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.fileUrl]];
        NSString *range = [NSString stringWithFormat:@"bytes=%zd-", self.totlalReceivedSize];
        [request setValue:range forHTTPHeaderField:@"Range"];
        
        self.task = [session dataTaskWithRequest:request];
        // 设置描述
        self.task.taskDescription = self.fileUrl;
    }
    
    /**
     *  恢复
     */
    - (void)resume
    {
        if (self.state == DownloadStateCompleted || self.state == DownloadStateResumed) return;
        
        [self.task resume];
        self.state = DownloadStateResumed;
    }
    
    /**
     * 等待下载
     */
    - (void)waitDownload
    {
        if (self.state == DownloadStateCompleted || self.state == DownloadStateWait) return;
        
        self.state = DownloadStateWait;
    }
    
    #pragma mark - 代理方法处理
    - (void)didReceiveResponse:(NSHTTPURLResponse *)response
    {
        // 获得文件总长度
        if (!self.totalExpectedSize) {
          NSInteger totalExpectedSize = [response.allHeaderFields[@"Content-Length"] integerValue] + self.totlalReceivedSize;
            
            // 存储文件总长度
            NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:self.totalFilesSizePath];
            if (dict == nil) dict = [NSMutableDictionary dictionary];
            
            dict[self.fileUrl] = @(totalExpectedSize);
            
           bool b =  [dict writeToFile:self.totalFilesSizePath atomically:YES];
            
            if (b == YES) {
                NSLog(@"%@",self.totalFilesSizePath);
            }
        }
        
        // 打开流
        [self.stream open];
        
        // 清空错误
        self.error = nil;
    }
    
    - (void)didReceiveData:(NSData *)data
    {
        // 写数据
        NSInteger result = [self.stream write:data.bytes maxLength:data.length];
        
        if (result == -1) {
            self.error = self.stream.streamError;
            [self.task cancel]; // 取消请求
        }else{
            self.thisTimeWrittenSize = data.length;
             [self notifyProgressChange]; // 通知进度改变
            
        }
    }
    
    - (void)notifyProgressChange
    {
        if (self.progressBlock) {
        
            self.progressBlock(self.thisTimeWrittenSize, self.totlalReceivedSize, self.totalExpectedSize);
            
        }
       
    }
    
    - (void)didCompleteWithError:(NSError *)error
    {
        // 关闭流
        [self.stream close];
        self.thisTimeWrittenSize = 0;
        self.stream = nil;
        self.task = nil;
        
        // 错误(避免nil的error覆盖掉之前设置的self.error)
        self.error = error ? error : self.error;
        
        // 通知(如果下载完毕 或者 下载出错了)
        if (self.state == DownloadStateCompleted || error) {
            // 设置状态
            self.state = error ? DownloadStateError : DownloadStateCompleted;
        }
    }
    
    /**
     *  暂停
     */
    - (void)suspend
    {
        if (self.state == DownloadStateCompleted || self.state == DownloadStateStoped) return;
        
        if (self.state == DownloadStateResumed) { // 如果是正在下载
            [self.task suspend];
            self.state = DownloadStateStoped;
        } else { // 如果是等待下载
            self.state = DownloadStateWait;
        }
    }
    
    
    
    @end
    
    

    YBDownloadManager

    //
    //  YBDownloadManager.h
    //  YBDownLoadDemo
    //
    //  Created by wyb on 2017/4/5.
    //  Copyright © 2017年 中天易观. All rights reserved.
    //
    
    #import <Foundation/Foundation.h>
    #import "YBFileModel.h"
    
    @interface YBDownloadManager : NSObject
    
    /** 最大同时下载数 */
    @property (assign, nonatomic) int maxDownloadingCount;
    
    + (instancetype)defaultManager;
    
    /**
     *  获得某个文件的下载信息
     *
     *  @param url 文件的URL
     */
    - (YBFileModel *)downloadFileModelForURL:(NSString *)url;
    
    /**
     *  下载一个文件
     *
     *  @param url          文件的URL路径
     *  @param progress     下载进度的回调
     *  @param state        状态改变的回调
     
     */
    - (YBFileModel *)download:(NSString *)url progress:(ProgressBlock)progress state:(StateBlock)state;
    
    /**
     *  暂停下载某个文件
     */
    - (void)suspend:(NSString *)url;
    
    /**
     *  全部文件暂停下载
     */
    - (void)suspendAll;
    
    /**
     * 全部文件开始\继续下载
     */
    - (void)resumeAll;
    
    @end
    
    
    //
    //  YBDownloadManager.m
    //  YBDownLoadDemo
    //
    //  Created by wyb on 2017/4/5.
    //  Copyright © 2017年 中天易观. All rights reserved.
    //
    
    #import "YBDownloadManager.h"
    
    // 缓存主文件夹,所有下载下来的文件都放在这个文件夹下
    #define YBCacheDirectory [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"YBCacheDir"]
    
    
    @interface YBDownloadManager ()<NSURLSessionDataDelegate>
    
    @property (strong, nonatomic) NSURLSession *session;
    /** 存放所有文件的下载信息 */
    @property (strong, nonatomic) NSMutableArray *downloadFileModelArray;
    
    @end
    
    @implementation YBDownloadManager
    
    
    
    static YBDownloadManager *_downloadManager;
    
    - (NSMutableArray *)downloadFileModelArray
    {
        if (_downloadFileModelArray == nil) {
            _downloadFileModelArray = [NSMutableArray array];
        }
        return _downloadFileModelArray;
    }
    
    + (instancetype)allocWithZone:(struct _NSZone *)zone
    {
        static dispatch_once_t onceToken;
        
        dispatch_once(&onceToken, ^{
            _downloadManager = [super allocWithZone:zone];
        });
        
        return _downloadManager;
    }
    
    - (nonnull id)copyWithZone:(nullable NSZone *)zone
    {
        return _downloadManager;
    }
    
    + (instancetype)defaultManager
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            _downloadManager = [[self alloc] init];
        });
        
        return _downloadManager;
    }
    
    - (YBFileModel *)downloadFileModelForURL:(NSString *)url
    {
        if (url == nil) {
            return  nil;
        }
        
        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"fileUrl==%@",url];
        
        YBFileModel *model = [[self.downloadFileModelArray filteredArrayUsingPredicate:predicate] firstObject];
        if (model == nil) {
            
            model = [[YBFileModel alloc]init];
            model.fileUrl = url;
            [self.downloadFileModelArray addObject:model];
            
        }
        
        return model;
    }
    
    - (YBFileModel *)download:(NSString *)url progress:(ProgressBlock)progress state:(StateBlock)state
    {
        if (url == nil) {
            return  nil;
        }
        
        YBFileModel *model = [self downloadFileModelForURL:url];
        model.progressBlock = progress;
        model.stateBlock = state;
        
        if (model.state == DownloadStateCompleted) {
            return model;
        }else if (model.state == DownloadStateResumed)
        {
            return model;
        }
        
        // 创建任务
        [model setupTask:self.session];
        
        //开始任务
        [self resume:url];
        
        return model;
        
    }
    
    - (NSURLSession *)session
    {
        if (!_session) {
            // 配置
            NSURLSessionConfiguration *cfg = [NSURLSessionConfiguration defaultSessionConfiguration];
            // session
            self.session = [NSURLSession sessionWithConfiguration:cfg delegate:self delegateQueue:[[NSOperationQueue alloc]init]];
        }
        return _session;
    }
    
    
    
    #pragma mark - <NSURLSessionDataDelegate>
    - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSHTTPURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
    {
        
        YBFileModel *model = [self downloadFileModelForURL:dataTask.taskDescription];
        
        // 处理响应
        [model didReceiveResponse:response];
        
        // 继续
        completionHandler(NSURLSessionResponseAllow);
    }
    
    - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
    {
        YBFileModel *model = [self downloadFileModelForURL:dataTask.taskDescription];
        
        // 处理数据
        [model didReceiveData:data];
    }
    
    - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
    {
        YBFileModel *model = [self downloadFileModelForURL:task.taskDescription];
        
        // 处理结束
        [model didCompleteWithError:error];
        
        // 让第一个等待下载的文件开始下载
        [self resumeFirstWillResume];
    }
    
    #pragma mark - 文件操作
    /**
     * 让第一个等待下载的文件开始下载
     */
    - (void)resumeFirstWillResume
    {
        
       YBFileModel *model = [self.downloadFileModelArray filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"state==%d", DownloadStateWait]].firstObject;
        [self resume:model.fileUrl];
    }
    
    - (void)resume:(NSString *)url
    {
        if (url == nil) return;
        
        YBFileModel *model = [self downloadFileModelForURL:url];
        
        // 正在下载的
        NSArray *downloadingModelArray = [self.downloadFileModelArray filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"state==%d", DownloadStateResumed]];
        if (self.maxDownloadingCount && downloadingModelArray.count == self.maxDownloadingCount) {
            // 等待下载
            [model waitDownload];
        } else {
            // 继续
            [model resume];
        }
    }
    
    
    - (void)suspend:(NSString *)url
    {
        if (url == nil) return;
        
        // 暂停
        [[self downloadFileModelForURL:url] suspend];
        
        // 取出第一个等待下载的
        [self resumeFirstWillResume];
    }
    
    /**
     *  全部文件暂停下载
     */
    - (void)suspendAll
    {
        [self.downloadFileModelArray enumerateObjectsUsingBlock:^(YBFileModel* model, NSUInteger idx, BOOL * _Nonnull stop) {
            
            [model suspend];
        }];
    }
    
    /**
     * 全部文件开始\继续下载
     */
    - (void)resumeAll
    {
        [self.downloadFileModelArray enumerateObjectsUsingBlock:^(YBFileModel* model, NSUInteger idx, BOOL * _Nonnull stop) {
            
            [self resume:model.fileUrl];
        }];
    }
    
    @end
    
    

    demo的下载地址 吻我😄😄😄

    相关文章

      网友评论

      本文标题:iOS开发之多任务文件下载,断点续传

      本文链接:https://www.haomeiwen.com/subject/emmpottx.html