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

疯狂的程序员决不是靠狂妄和拼命的程序员,而是能够脚踏实地,持续努力的程序员,一个程序员真正做到这两点,技术上去后,唯一能限制他的只有想象力,到那个时候才算“疯狂的程序员”,这种程序员,才能令对手无比恐惧。

美丽的风景图片


前言


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


文件的一次性下载


做应用程序的时候,不管我们是使用第三方网络请求类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 ()<NSURLSessionDataDelegate>

@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的传送门
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容