NSURLSession实现下载有两种方式,一种是通过NSURLSessionDataTask去实现,但是这个对象实现的下载是不支持后台下载的,但是他的断点续传是支持的很好。要想实现后台下载就必须使用NSURLSessionDownloadTask这个对象,今天先研究NSURLSessionDataTask的这个适合小文件的,在前台不用等几秒就能下载好的,比如下载个音乐文件,几兆的。如果要是做腾讯视频的大视频下载,得做后台下载。用户不会等很久的,这个需求是必须的。等有时间研究下NSURLSessionDownloadTask的。
要实现断点续传主要是把第一次下载的文件的大小记录下来。下次在请求头上告诉服务器从上次停止的地方开始下载。
还有个注意点就是下载的文件都放在内存会爆的,所以下载一点存储一点。
上代码前先写写用的类。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