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

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的下载地址 吻我😄😄😄

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

推荐阅读更多精彩内容