手机移动程序开发iOS开发者进阶iOS开发

iOS断点续传下载

2019-07-17  本文已影响87人  __Mr_Xie__

前言

之前在做 app 性能优化,发现下载一个大文件的时候,内存会飙升。看了一下代码才发现 前同事 采用的是一次性下载。

前同事 的对白:这不是我 Code Style,是 前同事 写的。

在进行下载时,如果是小文件的下载,比如小图片和文字之类的,我们可以直接请求源地址,然后一次下载完毕;但是如果是下载较大的图片、音频和视频文件时,不可能一次下载完毕,用户可能下载一段时间,关闭程序,回家接着下载。这个时候就需要使用断点续传进行下载。用户可以随时暂停下载,下次开始下载,还能接着上次的下载的进度。

原理

要实现断点续传的功能,通常都需要客户端记录下当前的下载进度,并在需要续传的时候通知服务端本次需要下载的内容片段。

HTTP1.1 协议(RFC2616)中定义了断点续传相关的 HTTP 头的 RangeContent-Range 字段,一个最简单的断点续传实现大概如下:

客户端下载一个 1024K 的文件,已经下载了其中 512K
网络中断,客户端请求续传,因此需要在 HTTP 头中申明本次需要续传的片段:
Range:bytes=512000-
这个头通知服务端从文件的 512K 位置开始传输文件
服务端收到断点续传请求,从文件的 512K 位置开始传输,并且在 HTTP 头中增加:
Content-Range:bytes 512000-/1024000
并且此时服务端返回的 HTTP 状态码应该是 206 ,而不是 200

实现方式

断点续传的实现有两种方式:

注:如果你想了解更多关于句柄的知识,可以阅读文章 iOS NSFileHandle

注:如果你想了解更多关于流的知识,可以阅读文章 iOS NSInputStream和NSOutputStream

demo效果及地址

demo地址

示例代码

:示例代码是下载一张比较大的图片,但是demo 在真机上运行时,如果网速过快可能看不到效果,如果想看到效果,可以把网络设置为 3GVery Bad Network ,设置如下图:

核心代码
#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
#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开发者进阶,直接留言,留言必回。

上一篇下一篇

猜你喜欢

热点阅读