华山论剑之浅谈 iOS 的文件下载, 断点下载 (基于 NSURLSession 的网络请求)

1,253 阅读7分钟
原文链接: www.jianshu.com

前言

上面的一张水域小镇风景图是那么的美丽,美丽的东西总是令人向往.现在我想它从网上下载下来当我的手机桌面的背景图,那么该怎么办?如果图片的很小,我们该如何做,如果图片过大我们又该如何处理呢?或者说是当我们需要下载一个几百兆的文件的时候,我们改如何处理呢?

文件的一次性下载

做应用程序的时候,不管我们是使用第三方网络请求类AFNetworking、ASIHTTPRequest,还是原生态的NSURLSession和NSURLConnection,我们请求后台数据大多数是一次请求完成的,现在我使用NSData自带的方法下载一下上面的图片.为了方便,我直接使用storyboard做的

控制器上的各个控件

"全部下载"按钮的代码如下.
//一次性下载所有数据
- (IBAction)loadAllData:(id)sender {

    //使用NSData 直接下载文件
    NSURL *urlString = [NSURL URLWithString:@"http://www.deskcar.com/desktop/fengjing/20125700336/18.jpg"];

    NSData *data = [NSData dataWithContentsOfURL:urlString];

    NSLog(@"%@",data);

    self.imageView.image  = [UIImage imageWithData:data];

    self.imageView.contentMode = UIViewContentModeScaleAspectFit;


}
当然,这里我直接使用的主线程请求网络数据,其实应该开辟一个子线程做请求网络数据,但是我们在主线程中可以轻易的看到 "全部下载"按钮的卡顿(如下图),造成的原因一个是图片文件太大,另外一个就是没有开辟子线程,文件太大的时候,我们就可以使用断点下载了.

按钮的卡顿现象严重

大文件的直接下载和断点下载

大文件的下载在这里我说一下 iOS原生态网络请求类NSURLSession 的直接下载和断点下载,NSURLConnection由于这个类已经被弃用了,所以我就不多言语了.
直接下载 我们不能再用以前的简单粗暴的方法直接把从网络中请求到的数据直接放到内存中,那样的话,会严重影响到程序中的其他功能.我们应该直接把请求到的数据直接放到沙盒当中,进行数据的持久化.对于直接下载我们用到的是NSURLSessionTask的子类NSURLSessionDownloadTask,不管是直接下载还是断点续传,我们都需要遵守NSURLSessionDownloadDelegate协议,并且对协议中的方法进行实现.
注意 : 使用代理方法实现网络请求的时候,不能同时再使用网络请求对象中的block块,因为block的优先级高于代理方法,所以同时使用代理方法是不执行的!!!

我们看一下文件的直接下载的时候,我们都需要用到那几个代理方法.

写入数据
/**
 *  
 *
 *  @param session
 *  @param downloadTask              当前下载任务
 *  @param bytesWritten              当前这次写入数据的大小
 *  @param totalBytesWritten         已经写入数据的大小
 *  @param totalBytesExpectedToWrite 预计写入数据的总大小
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{

}

`
下载完成时
/**
 *  
 *
 *  @param session
 *  @param downloadTask 当前下载任务 (属性中有响应头)
 *  @param location     下载的位置
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {

}
完成文件下载任务
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{      

   }
还是拿上面的那张壁纸的URL为例(壁纸的大小大约有1.2M),我们对其直接做网络下载.不说话,直接上代码.
#pragma mark --- 文件直接下载 ----

- (IBAction)breakpointData:(id)sender {

    //设置代理
    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];


    NSURL *urlString = [NSURL URLWithString:@"http://www.deskcar.com/desktop/fengjing/20125700336/18.jpg"];

    NSURLSessionDownloadTask *downLoadTask = [session downloadTaskWithURL:urlString];

    //启动下载任务
    [downLoadTask resume];

    self.progressView = [MBProgressHUD showHUDAddedTo:self.view animated:YES];

    // Set the bar determinate mode to show task progress.
    self.progressView.mode = MBProgressHUDModeDeterminateHorizontalBar;
    self.progressView.label.text = @"文件下载中....";

}




#pragma mark - NSURLSessionDownloadDelegate

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
    //MBProgressHUD进度条显示
    self.progressView.progress = (float)1.0*totalBytesWritten / totalBytesExpectedToWrite ;

}


- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
    //根据请求头中的文件名在沙盒中直接创建路径
    NSURLResponse *response = downloadTask.response;

    NSString *filePaths =[self cacheDir:response.suggestedFilename];

    self.filePaths = filePaths;

    NSFileManager *fileManager = [NSFileManager defaultManager];

    //将临时的下载文件(在内存中)放入沙盒中.
    [fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:filePaths] error:nil];

}

// 完成任务
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    if (self.progressView.progress == 1.0) {

        self.imageView.image = [UIImage imageWithContentsOfFile:self.filePaths];

        [self.progressView hideAnimated: YES];
    }
}


#pragma mark --- 输入一个字符串,则在沙盒中生成路径
// 传入字符串,直接在沙盒Cache中生成路径
- (NSString *)cacheDir:(NSString *)paths
{
    NSString *cache = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;

    return [cache stringByAppendingPathComponent:[paths lastPathComponent]];
}
断点下载 断点下载的核心以及和直接下载的区别就是请求头Rang.我们先看些关于Rang相关的知识.

只要设置HTTP请求头的Range属性, 就可以实现从指定位置开始下载
表示头100个字节:Range: bytes=0-99
表示第二个100字节:Range: bytes=100-199
表示最后100个字节:Range: bytes=-100
表示100字节以后的范围:Range: bytes=100-

如下设置请求头
//设置请求头 ,这个是从什么位置开始到最后,不懂看上面的Range属性的设置

NSString *range = [NSString stringWithFormat:@"bytes=%ld-",self.currentLength]; 

[request setValue:range forHTTPHeaderField:@"Range"];
当使用NSURLSessionDownloadTask的时候,我们就可以不用设置请求头,因为系统给封装了两个方法,使我们可以更简单的进行断点续传.
  • 一个是任务暂停时候的的带有block回调函数的方法,方法中有个NSData类型的参数resumeData是用于记录下载的URL地址和已下载的总共的字节数两部分,而不是直接存储的已下载的数据.我们需要做的就是把resumeData保存下来,用于后面的断点续传.
- (void)cancelByProducingResumeData:(void (^)(NSData * __nullable resumeData))completionHandler;
  • 另外一个就是NSURLSession 自带的使用resumeData创建NSURLSessionDownloadTask的初始化方法.我们只要把上面的resumeData的传过来创建就可以了.
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData;
当然,我们还是要对下载进队做监控,那么还是要实现NSURLSessionDownloadDelegate的协议中的方法.那么不多说,直接上代码和原型图.
#import "ViewController.h"

@interface ViewController ()

@property (strong, nonatomic) IBOutlet UIImageView *imageView;//图片

@property (strong, nonatomic) IBOutlet UIButton *breakpointButton;

@property (strong, nonatomic) IBOutlet UILabel *progressLabel;

@property(nonatomic,strong)NSString *filePaths;//文件的沙盒路径

@property(nonatomic,assign)NSInteger fileSize;//本地已经下载的文件的大小

@property(nonatomic,assign)NSInteger altogetherSize;//文件总共的大小

@property (nonatomic, strong) NSURLSessionDownloadTask *task;

@property (nonatomic, strong) NSData *resumeData;

@property (nonatomic, strong) NSURLSession *session;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.imageView.contentMode = UIViewContentModeScaleAspectFit;//图片大小自适应

    self.filePaths = 0;

    self.fileSize = 0;

    self.altogetherSize = 0;

}


#pragma mark --- 断点下载 --- 

- (IBAction)breakpointData:(UIButton *)sender {

    if (self.task == nil) { // 开始(继续)下载

        if (self.resumeData) { // 恢复

            [sender setTitle:@"暂停" forState:UIControlStateNormal];

            [self resume];
        } else { // 开始
            [self start];

            [sender setTitle:@"暂停" forState:UIControlStateNormal];

        }
    } else { // 暂停

        [sender setTitle:@"继续" forState:UIControlStateNormal];

        [self pause];
    }


}


//懒加载
- (NSURLSession *)session
{
    if (!_session) {
        // 获得session
        NSURLSessionConfiguration *cfg = [NSURLSessionConfiguration defaultSessionConfiguration];
        self.session = [NSURLSession sessionWithConfiguration:cfg delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    }
    return _session;
}



- (void)start
{
    // 1.创建一个下载任务
    NSURL *url = [NSURL URLWithString:@"http://www.deskcar.com/desktop/fengjing/20125700336/18.jpg"];
    self.task = [self.session downloadTaskWithURL:url];

    // 2.开始任务
    [self.task resume];
}


- (void)resume
{
    // 传入上次暂停下载返回的数据,就可以恢复下载
    self.task = [self.session downloadTaskWithResumeData:self.resumeData];

    // 开始任务
    [self.task resume];

    // 清空
    self.resumeData = nil;
}


- (void)pause
{
    __weak typeof(self) vc = self;
    [self.task cancelByProducingResumeData:^(NSData *resumeData) {
        //  resumeData : 包含了继续下载的开始位置\下载的url
        vc.resumeData = resumeData;
        vc.task = nil;
    }];
}

#pragma mark - NSURLSessionDownloadDelegate
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
    //根据请求头中的文件名在沙盒中直接创建路径
    NSURLResponse *response = downloadTask.response;

    NSString *filePaths =[self cacheDir:response.suggestedFilename];

    self.filePaths = filePaths;

    NSFileManager *fileManager = [NSFileManager defaultManager];

    //将临时的下载文件(在内存中)放入沙盒中.
    [fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:filePaths] error:nil];

    self.imageView.image = [UIImage imageWithContentsOfFile:self.filePaths];




}

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
      didWriteData:(int64_t)bytesWritten
 totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{

    if (totalBytesExpectedToWrite > self.altogetherSize) {

        self.altogetherSize = totalBytesExpectedToWrite;

        NSLog(@"%ld",(long)self.altogetherSize);
    }

    NSLog(@"%f",(double)totalBytesWritten / self.altogetherSize);

    self.progressLabel.text = [NSString stringWithFormat:@"%.0f %",(double)100*totalBytesWritten / self.altogetherSize];

    if ((double)totalBytesWritten / self.altogetherSize == 1) {


        //关掉用户交互
        [self.breakpointButton setTitle:@"完成" forState:UIControlStateNormal];


        self.breakpointButton.userInteractionEnabled = NO;

    }
}



#pragma mark --- 输入一个字符串,则在沙盒中生成路径
// 传入字符串,直接在沙盒Cache中生成路径
- (NSString *)cacheDir:(NSString *)paths
{
    NSString *cache = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject;

    return [cache stringByAppendingPathComponent:[paths lastPathComponent]];
}


@end

开始界面


下载过程中


完成页面

总结: 断点续传以及大文件的下载在我们的程序开发过程中时常用到,用途比较广泛,比如开发一个应用商店,一个书架App等等,而且NSURLSession的断点续传比较简单.希望这篇文章对您的开发能有所帮助.最后附上自己做的Demo,不懂在评论区回复,我会及时回复您,谢谢.

------ > 🚀Demo的传送门