iOS断点续传下载

作者: __Mr_Xie__ | 来源:发表于2019-07-17 16:18 被阅读87次

    前言

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

    实现方式

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

    • 通过句柄(NSFileHandle)的方式实现;

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

    • 通过流(NSOutputStream)的方式实现;

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

    demo效果及地址

    demo地址

    示例代码

    :示例代码是下载一张比较大的图片,但是demo 在真机上运行时,如果网速过快可能看不到效果,如果想看到效果,可以把网络设置为 3GVery 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开发者进阶,直接留言,留言必回。

    相关文章

      网友评论

        本文标题:iOS断点续传下载

        本文链接:https://www.haomeiwen.com/subject/tmkqlctx.html