注意点:
#1,文件的保存路径不能是网址!!!
#2,NSOutputStream的作用就是保存网路下载的数据。
1,NSHTTPURLResponse响应对象
1,statusCode:状态码,可以根据这个值判断是否请求出错。
2,allHeaderFields:获得响应体内容
3,URL:一般使用在重定向,如果不需要重定向,响应的url和请求的url是一样的。
4,MIMEType:服务器告诉客户端返回的数据类型
5,textEncodingName :服务器告诉客户端返回内容的编码格式
6,expectedContentLength:服务器返回数据的长度,客户端可以通过该属性获得文件大小
7,suggestedFilename:服务器建议客户端保存文件使用的名字
2,参考:iOS中流(NSStream)的使用
#NSInputStream 和 NSOutputStream
1,NSInputStream 和 NSOutputStream 是NSStream的两个子类,分别对应了读文件和 写文件。
2,NSInputStream 和 NSOutputStream其实是对 CoreFoundation 层对应的CFReadStreamRef 和 CFWriteStreamRef 的高层抽象。
3,断点续传
#1,NSURLSessionTask是一个抽象类,本身不能使用,只能使用它的子类
NSURLSessionDataTask((完美断点)下载)、
NSURLSessionUploadTask(上传)、
NSURLSessionDownloadTask((有缺陷断点)下载)
注意:请求的时候,只要有completionHandler结果回调的,那么都不会走代理方法。只有下载完成之后才会走completionHandler回调。
#2,NSURLSessionDownloadTask实现断点下载(有缺陷)
//如果任务,取消了那么以后就不能恢复了
// [self.downloadTask cancel];
//如果采取这种方式来取消任务,那么该方法会通过resumeData保存当前文件的下载信息
//只要有了这份信息,以后就可以通过这些信息来恢复下载
[self.downloadTask cancelByProducingResumeData:^(NSData * __nullable resumeData) {
self.resumeData = resumeData;
}];
-----------
//继续下载
//首先通过之前保存的resumeData信息,创建一个下载任务
self.downloadTask = [self.session downloadTaskWithResumeData:self.resumeData];
[self.downloadTask resume];
局限性:
01 如果用户点击暂停之后退出程序,那么需要把恢复下载的数据写一份到沙盒,代码复杂度增加
02 如果用户在下载中途未保存恢复下载数据即退出程序,则不具备可操作性
#3,NSURLSessionDownloadTask两种请求方法介绍
// 1,该方法不会默认保存存到沙盒tmp文件中(可以通过NSURLSessionDownloadTask并以代理的方式来完成大文件的下载,但是需要手动存到沙盒中)
NSURLSessionDownloadTask *downloadTask = [self.session downloadTaskWithURL:url];
// 2,内部默认已经实现了边下载边写入沙盒tmp文件中操作,所以不用开发人员担心内存问题
//注意,tmp中存储的后缀是.tmp,可以通过剪切文件来修改后缀。
// 缺点:不能监听下载的进度。
// NSURLSessionDownloadTask *downloadTask = [self.session downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {}];
4,任务的三种操作
#启动task:
[dataTask resume];
#取消task
[dataTask cancel];
#暂停task
[dataTask suspend];
5,实现断点下载最好的方式是使用NSURLSessionDataTask实现大文件离线断点下载
#流程:
文件是否下载完成-->文件下载中还是暂停下载-->是否存在文件下载的目录-->创建流,用NSURLSessionDataTask实现断点下载(如下)
#注意:一个下载文件URL需要两部分:task和stream
#HSFileName(url):表示通过URL加密后对应的文件名
#通过taskIdentifier获取task的时候,需要强转一下
//根据url获得对应的下载任务
- (NSURLSessionDataTask *)getTask:(NSString *)url
{
return (NSURLSessionDataTask *)[self.tasks valueForKey:HSFileName(url)];
}
#正文:
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
// 创建流
NSOutputStream *stream = [NSOutputStream outputStreamToFileAtPath:HSFileFullpath(url) append:YES];
// 创建请求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]];
// 设置请求头(从上次下载的长度开始继续下载)
NSString *range = [NSString stringWithFormat:@"bytes=%zd-", HSDownloadLength(url)];
[request setValue:range forHTTPHeaderField:@"Range"];
// 创建一个Data任务(若之前已经下载过一部分的文件了,那么这里是从上次下载到的位置开始下载的,这里创建的task就会赋值上新的taskIdentifier,并且把原来URL对应的task替换掉。)
NSURLSessionDataTask *task = [session dataTaskWithRequest:request];
NSUInteger taskIdentifier = arc4random() % ((arc4random() % 10000 + arc4random() % 10000));
[task setValue:@(taskIdentifier) forKeyPath:@"taskIdentifier"];
// 保存任务
[self.tasks setValue:task forKey:HSFileName(url)];
HSSessionModel *sessionModel = [[HSSessionModel alloc] init];
sessionModel.url = url;
sessionModel.progressBlock = progressBlock;
sessionModel.stateBlock = stateBlock;
sessionModel.stream = stream;
[self.sessionModels setValue:sessionModel forKey:@(task.taskIdentifier).stringValue];
//开始下载(然后就到代理方法中进行操作即可)
[self start:url];
#pragma mark - 代理
#pragma mark NSURLSessionDataDelegate
/**
* 接收到响应(进行打开下载任务对应的流,然后保存该文件的总大小到本地。)
*/
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSHTTPURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
HSSessionModel *sessionModel = [self getSessionModel:dataTask.taskIdentifier];
// 打开流
[sessionModel.stream open];
// 获得服务器这次请求 返回数据的总长度(Content-Length对应的是当前下载的URL对应的文件大小,也许文件已经下载过一部了,现在是继续下载的,所以这里后面又加上了已经下载的文件的大小)
NSInteger totalLength = [response.allHeaderFields[@"Content-Length"] integerValue] + HSDownloadLength(sessionModel.url);
sessionModel.totalLength = totalLength;
//获取plist文件 (存储总长度到plist文件中,HSTotalLengthFullpath:plist文件的路径)
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:HSTotalLengthFullpath];
if (dict == nil) dict = [NSMutableDictionary dictionary];
dict[HSFileName(sessionModel.url)] = @(totalLength);
[dict writeToFile:HSTotalLengthFullpath atomically:YES];
// 接收这个请求,允许接收服务器的数据。(因为系统默认是不响应下载任务的)
completionHandler(NSURLSessionResponseAllow);
}
/**
* 接收到服务器返回的数据
*/
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
HSSessionModel *sessionModel = [self getSessionModel:dataTask.taskIdentifier];
// 写入数据
[sessionModel.stream write:data.bytes maxLength:data.length];
// 下载进度
NSUInteger receivedSize = HSDownloadLength(sessionModel.url); //已经下载的文件的大小
NSUInteger expectedSize = sessionModel.totalLength; //文件的总大小
CGFloat progress = 1.0 * receivedSize / expectedSize;
sessionModel.progressBlock(receivedSize, expectedSize, progress);
}
/**
* 请求完毕(成功|失败)
*/
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
HSSessionModel *sessionModel = [self getSessionModel:task.taskIdentifier];
if (!sessionModel) return;
if ([self isCompletion:sessionModel.url]) {
// 下载完成
sessionModel.stateBlock(DownloadStateCompleted);
} else if (error){
// 下载失败
sessionModel.stateBlock(DownloadStateFailed);
}
// 关闭流
[sessionModel.stream close];
sessionModel.stream = nil;
// 清除任务
[self.tasks removeObjectForKey:HSFileName(sessionModel.url)];
[self.sessionModels removeObjectForKey:@(task.taskIdentifier).stringValue];
}