首先做断点续传 我们首先需要考虑有什么功能。
原理:
要实现断点续传的功能,通常需要客户端记录当前下载的下载进度,并在需要续传的时候通知服务端本次需要下载的内容片断。
在HTTP1.1协议(RFC2616)中定义了断点续传相关的HTTP头的Range和Content-Range 字段。一个简单的断点续传实现大概如下:
1、客户端下载一个1024K的文件,已经下载了其中512K
2.网络中断,客户端请求续传,因此需要在HTTP头中申明需要续传的片段:Range:bytes=512000-这个头通知服务端从文件的512K位置开始传输文件。
3.服务端收到断点续传请求,从文件512K位置开始传输,并且在HTTP头中增加:Content-Range:bytes 512000-1024000 并且此时服务端返回的HTTP状态码应该是206而不是200.
客户端如何获取已经下载的文件字节数
客户端这边,我们需要记录下每次用户每次下载的文件大小,然后实现原谅你讲解中步骤1的功能。
直接获取指定路径下的文件大小 iOS提供了相关功能
[[[NSFileManager defaultManager] attributesOfItemAtPath: FileStorePath error:nil][NSFileSize] integerValue]
如何获取被下载文件的总字节数
我们获取了已经下载文件的字节数,这里我们需要获取被下载文件的总字节数,有了这两个值,我们就可以算出下载进度了。
使用http头部的content-length字段
Content-Length用于描述HTTP消息实体的传输长度the transfer-length of the message-body。在HTTP协议中,消息实体长度和消息实体的传输长度是有区别,比如说gzip压缩下,消息实体长度是压缩前的长度,消息实体的传输长度是gzip压缩后的长度。
简单点说,content-length表示被下载文件的字节数。
对比原理讲解的第三步,我们可以看到如果要计算出文件的总字节数,那么必须把已经下载的字节数 加上 content-length。
我们需要把每个被下载文件的总字节数存储起来,这里我们选择使用plist文件来记载,plist文件包含一个字典。设置文件名为键值,已经下载的文件字节数为值。
文件名为了防止重复,这里我们设置文件名为下载url的hash值,可以保证不重重。
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSHTTPURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
self.totalLength = [response.allHeaderFields[@"Content-Length"] integerValue] + DownloadLength;
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile: TotalLengthPlist];
if (dict == nil) dict = [NSMutableDictionary dictionary];
dict[ Filename] = @(self.totalLength);
[dict writeToFile: TotalLengthPlist atomically:YES];
}
// 设置请求头
// Range : bytes=xxx-xxx,从已经下载的长度开始到文件总长度的最后都要下载
NSString *range = [NSString stringWithFormat:@"bytes=%zd-", DownloadLength];
[request setValue:range forHTTPHeaderField:@"Range"];
断点续传1.png
首先我们需要明确的几个功能
- 开始下载
- 暂停下载
- 恢复下载
- 清除下载
- 断点续传
定义好功能后我们开始从开始下载这个功能开始
-
开始下载
1.创建一个枚举来保存下载状态方便外界到时候调用。 2.先对连接进行容错处理 3.定义三个数组来保存下载任务。这个的主要作用是保存任务和设置并发数量。 4.需要建立数据库进行状态的保存
1.开始下载 -> 更改状态为下载状态 -> 假如等待下载数组中 -> 判断是否符合最大任务数据的下载 -> for循环遍历要下载的任务数
NSInteger willBeginTaskCount = self.maxDownloadingTaskCount - self.downloadingList.count;
for循环遍历 取出等待下载列表中的第一个任务。 将其从等待列表中删除 假如到正在下载列表中 -> 开始下载
下载采用模型的方式进行传递这里遇见的一个问题是一个模型中有三个子文件需要下载
采用多线程dispatch_group_enter dispatchNotify的形式来进行保证模型中的所有文件都下载完毕再通知主线程该文件下载完毕。
分割线进入正题
断点下载续传可以使用两种方式 一种是直接使用AFN的形式进行下载 一种是使用NSURLSessionDownLoadTask 进行下载
这边使用NSURLSessionDownLoadTask 进行下载来实现断点续传以及后台下载。这里需要强调说明下
断点续传和后台下载是两个不同的功能
这边先讲断点续传
这里通过本地保存resumeData来实现断点续传。
- NSURLSession的理解 一个session可以对应多个downloadTask 但是一个下载任务只能有唯一的一个downLoadTask进行下载。
- 为此这里建立一个下载工具类进行管理。
- HETDownloadUrlSession 管理类 该类主要提供2个API 给外界使用
- 1.开始下载 2.暂停下载
-(void)downloadTaskWithUrl:(NSString *)url taskId:(NSString *)taskId filePath:(NSString *)downloadFilePath progress:(Progress)progress success:(Success)successBlock fail:(Failure)failBlock; - (void)removeOperationWithTaskId:(NSString *)taskID isCancelOperation:(BOOL)isCancel;
6.使用单例来初始化session 这里使用单例有个坑就是 因为单例是全局的。你一个模型中有三个子文件需要下载。这样你需要将每个下载成功回调给外面。如果你单纯的使用上面那个success回调。那么下载一个文件时没有问题的。但是如果你同时下载两个文件的时候就会出问题了。
为啥呢。
因为当你下载任务的时候由于你的单例是全局的导致你每次回调的success progress failBlock 的地址都是一样的。 你每次下载完毕都回调外面给外面告诉外界你下载完毕。由于你采用的是 groupEnter -groupLeave 导致当你下载完第一个的时候第二个还是用回来的回调。导致你的leave的次数不对称。崩溃。(这个问题当时一直没考虑到地址的问题。以为你多线程并发导致的。)通过打印group这个字典以及回调block的地址发现问题了。
解决方案 通过每次创建任务的时候 新建一个模型 每个模型中保存一个下载进度 成功 失败的回调 这样就解决了。这个问题 。perfect。
回到主题
单例里面初始化一个字典 该字典用来存储resumeData 每次判断下载的时候
先判断下是否有resumeData 如果有则进行断点续传 没有则进行新建任务。
哪里保存resumData呢。在暂停的时候
//取消任务
[downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
//存储resumeData
[self downloadSaveResumeData:taskID resumeData:resumeData];
}];
这样就完成了断点续传的任务
后台下载:
用到了// 后台下载标识
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"HETDownloadBackgroundSessionIdentifier"];
这里需要注意identifier 需要唯一。因为你用的代理到时候需要用到这个。这里才是采用单例的原因。
实现代理方法:
#pragma mark - NSURLSessionDelegate
// 应用处于后台,所有下载任务完成及NSURLSession协议调用之后调用
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session
{
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
if (appDelegate.backgroundSessionCompletionHandler) {
void (^completionHandler)(void) = appDelegate.backgroundSessionCompletionHandler;
appDelegate.backgroundSessionCompletionHandler = nil;
// 执行block,系统后台生成快照,释放阻止应用挂起的断言
completionHandler();
}
}
在APPdelegate 中@property (nonatomic, copy) void (^ backgroundSessionCompletionHandler)(void); // 后台所有下载任务完成回调
// 应用处于后台,所有下载任务完成调用
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)(void))completionHandler
{
_backgroundSessionCompletionHandler = completionHandler;
}








网友评论