iOS断点续传下载

it2022-05-05  136

前言

之前在做 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地址

 

示例代码

注:示例代码是下载一张比较大的图片,但是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开发者进阶,直接留言,留言必回。

 


最新回复(0)