iOS断点续传下载
前言
之前在做 app
性能优化,发现下载一个大文件的时候,内存会飙升。看了一下代码才发现 前同事
采用的是一次性下载。
前同事
的对白:这不是我 Code Style
,是 前同事
写的。
在进行下载时,如果是小文件的下载,比如小图片和文字之类的,我们可以直接请求源地址,然后一次下载完毕;但是如果是下载较大的图片、音频和视频文件时,不可能一次下载完毕,用户可能下载一段时间,关闭程序,回家接着下载。这个时候就需要使用断点续传进行下载。用户可以随时暂停下载,下次开始下载,还能接着上次的下载的进度。
原理
要实现断点续传的功能,通常都需要客户端记录下当前的下载进度,并在需要续传的时候通知服务端本次需要下载的内容片段。
在 HTTP1.1
协议(RFC2616
)中定义了断点续传相关的 HTTP
头的 Range
和 Content-Range
字段,一个最简单的断点续传实现大概如下:
客户端下载一个 1024K
的文件,已经下载了其中 512K
网络中断,客户端请求续传,因此需要在 HTTP
头中申明本次需要续传的片段:
Range:bytes=512000-
这个头通知服务端从文件的 512K
位置开始传输文件
服务端收到断点续传请求,从文件的 512K
位置开始传输,并且在 HTTP
头中增加:
Content-Range:bytes 512000-/1024000
并且此时服务端返回的 HTTP
状态码应该是 206
,而不是 200
。
实现方式
断点续传的实现有两种方式:
- 通过句柄(
NSFileHandle
)的方式实现;
注:如果你想了解更多关于句柄的知识,可以阅读文章 iOS NSFileHandle
- 通过流(
NSOutputStream
)的方式实现;
注:如果你想了解更多关于流的知识,可以阅读文章 iOS NSInputStream和NSOutputStream
demo效果及地址
示例代码
注
:示例代码是下载一张比较大的图片,但是demo
在真机上运行时,如果网速过快可能看不到效果,如果想看到效果,可以把网络设置为 3G
或 Very Bad Network
,设置如下图:
核心代码
- 通过句柄(
NSFileHandle
)的方式实现断点续传;
#import "ViewControllerOne.h"
@interface ViewControllerOne () <NSURLConnectionDataDelegate>
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;
@property (nonatomic, strong) NSURLConnection *connection;
// 沙盒路径
@property (nonatomic, strong) NSString *fullPath;
@property (nonatomic, strong) NSString *fileName;
@property (nonatomic, assign) NSInteger totalSize;
@property (nonatomic, assign) NSInteger currentSize;
@property (nonatomic, strong) NSFileHandle *handle;
@end
@implementation ViewControllerOne
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"通过句柄实现断点续传";
}
- (IBAction)startDownloadAction:(UIButton *)sender {
[self download];
}
- (IBAction)cancelDownloadAction:(UIButton *)sender {
[self.connection cancel];
}
- (IBAction)goOnDownloadAction:(UIButton *)sender {
[self download];
}
- (void)download {
NSString *urlString = [@"https://desk-fd.zol-img.com.cn/t_s2880x1800c5/g2/M00/0A/08/ChMlWl0etgeIBDlZABHLgESTo1gAALjkAAAAAAAEcuY500.jpg" stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
NSURL *url = [NSURL URLWithString:urlString];
// 创建请求对象
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 设置请求头信息,告诉服务器值请求一部分数据的range
/*
设置请求头信息有固定格式
eg:
表示头500个字节:Range: bytes=0-499
表示第二个500字节:Range: bytes=500-999
表示最后500个字节:Range: bytes=-500
表示500个字节以后的范围:Range: bytes=500-
*/
NSString *range = [NSString stringWithFormat:@"bytes=%zd-", self.currentSize];
[request setValue:range forHTTPHeaderField:@"Range"];
// 发送请求
/*
参数1: 文件路径
参数2: YES 追加
特点:如果该输出流指向的地址没有文件,那么会自动创建一个空文件
*/
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
}
#pragma mark - NSURLConnectionDataDelegate
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
NSLog(@"%s", __func__);
// 得到文件的总大小
// 注:本次请求的文件数据的总大小 不等于 文件的总大小
self.totalSize = self.currentSize + response.expectedContentLength;
if (self.currentSize > 0) {
return;
}
// 根据响应头的信息获得推荐的文件名称
// suggestedFilename: 服务器端推荐的名称,其实就是URL的最后一个节点
self.fileName = response.suggestedFilename;
// 获得caches目录
NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
// 拼接全路径
self.fullPath = [caches stringByAppendingPathComponent:self.fileName];
// 新建一个空的文件
/*
参数1: 文件的路径
参数2: 文件的内容
参数3: 文件的属性
*/
[[NSFileManager defaultManager] createFileAtPath:self.fullPath contents:nil attributes:nil];
// 创建文件句柄
self.handle = [NSFileHandle fileHandleForWritingAtPath:self.fullPath];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// NSLog(@"%s", __func__);
// 移动文件句柄到文件末尾
[self.handle seekToEndOfFile];
// 写数据到磁盘
[self.handle writeData:data];
// 获得进度
self.currentSize += data.length;
NSLog(@"%f", 1.0 * self.currentSize / self.totalSize);
self.progressView.progress = 1.0 * self.currentSize / self.totalSize;
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(@"%s", __func__);
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(@"%@", self.fullPath);
NSLog(@"%s", __func__);
// 释放文件句柄
[self.handle closeFile];
self.handle = nil;
}
@end
- 通过流(
NSOutputStream
)的方式实现断点续传;
#import "ViewController.h"
@interface ViewController () <NSURLConnectionDataDelegate>
@property (nonatomic, strong) NSURLConnection *connection;
// 沙盒路径
@property (nonatomic, strong) NSString *fullPath;
@property (nonatomic, assign) NSInteger totalSize;
@property (nonatomic, assign) NSInteger currentSize;
// 输出流
@property (nonatomic, strong) NSOutputStream *stream;
@property (weak, nonatomic) IBOutlet UIProgressView *progress;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"通过流实现断点续传";
}
- (IBAction)startDownloadAction:(UIButton *)sender {
NSLog(@"\n\r\n\r -----------开始下载----------- \n\r");
[self download];
}
- (IBAction)cancelDownloadAction:(UIButton *)sender {
NSLog(@"\n\r\n\r -----------取消下载----------- \n\r");
[self.connection cancel];
}
- (IBAction)goOnDownloadAction:(UIButton *)sender {
NSLog(@"\n\r\n\r -----------继续下载----------- \n\r");
[self download];
}
- (void)download {
NSString *urlString = [@"https://desk-fd.zol-img.com.cn/t_s2880x1800c5/g2/M00/0A/08/ChMlWl0etgeIBDlZABHLgESTo1gAALjkAAAAAAAEcuY500.jpg" stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];
NSURL *url = [NSURL URLWithString:urlString];
// 创建请求对象
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 设置请求头信息,告诉服务器值请求一部分数据的range
/*
设置请求头信息有固定格式
eg:
表示头500个字节:Range: bytes=0-499
表示第二个500字节:Range: bytes=500-999
表示最后500个字节:Range: bytes=-500
表示500个字节以后的范围:Range: bytes=500-
*/
NSString *range = [NSString stringWithFormat:@"bytes=%zd-", self.currentSize];
[request setValue:range forHTTPHeaderField:@"Range"];
// 发送请求
/*
参数1: 文件路径
参数2: YES 追加
特点:如果该输出流指向的地址没有文件,那么会自动创建一个空文件
*/
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
}
#pragma mark - NSURLConnectionDataDelegate
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
NSLog(@"%s", __func__);
// 得到文件的总大小
// 注:本次请求的文件数据的总大小 不等于 文件的总大小
self.totalSize = self.currentSize + response.expectedContentLength;
if (self.currentSize > 0) {
return;
}
// 写数据到沙盒中
self.fullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"123.jpg"];
self.stream = [[NSOutputStream alloc] initToFileAtPath:self.fullPath append:YES];
// 打开输入流
[self.stream open];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// NSLog(@"%s", __func__);
// 写数据
[self.stream write:data.bytes maxLength:data.length];
// 获得进度
self.currentSize += data.length;
NSLog(@"%f", 1.0 * self.currentSize / self.totalSize);
self.progress.progress = 1.0 * self.currentSize / self.totalSize;
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(@"%s", __func__);
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(@"%s", __func__);
// 关闭流
[self.stream close];
self.stream = nil;
}
@end
Author
如果你有什么建议,可以关注我的公众号:iOS开发者进阶
,直接留言,留言必回。