iOS开发网络篇之文件下载、大文件下载、断点下载

这里写图片描述

iOS开发中经常会用到文件的下载与上传功能,今天咱们来分享一下文件下载的思路。文件上传下篇再说。

文件下载分为小文件下载与大文件下载

小文件下载

小文件可以是一张图片,或者一个文件,这里指在现行的网络状况下基本上不需要等待很久就能下载好的文件。这里以picjumbo里的一张图片为例子。

NSData方式

其实我们经常用的[NSData dataWithContentsOfURL] 就是一种文件下载方式,猜测这里面应该是发送了Get请求。

NSURL *url = [NSURL URLWithString:@"https://picjumbo.imgix.net/HNCK8461.jpg?q=40&w=1650&sharp=30"];
NSData *data = [NSData dataWithContentsOfURL:url];

当然下载代码应该放到子线程执行

NSURLConnection方式下载

NSURL* url = [NSURL URLWithString:@"https://picjumbo.imgix.net/HNCK8461.jpg?q=40&w=1650&sharp=30"];
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:url] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
        
self.imageView.image = [UIImage imageWithData:data];
    }];

就是发送一个异步的Get请求,回调的data就是我们下载到的图片。
这些都很简单,今天主要说的是大文件的下载。

大文件下载

NSURLConnection下载

通过上面的两个方法去下载大文件是不合理的,因为这两个方法都是一次性返回整个下载到的文件,返回的data在内存中,如果下载一个几百兆的东西,内存肯定会爆的。
其实NSURLConnection还提供了另外一种发送请求的方式

// 发送请求去下载 (创建完conn对象后,会自动发起一个异步请求)
[NSURLConnection connectionWithRequest:request delegate:self];

这里用到了代理,那肯定要遵守协议了.遵守NSURLConnectionDataDelegate 协议.
进去看看有几个代理方法,其实我们能用到的也就三个。

/**
 *  请求失败时调用(请求超时、网络异常)
 *
 *  @param error      错误原因
 */
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    
}
/**
 *  1.接收到服务器的响应就会调用
 *
 *  @param response   响应
 */
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    
}
/**
 *  2.当接收到服务器返回的实体数据时调用(具体内容,这个方法可能会被调用多次)
 *
 *  @param data       这次返回的数据
 */
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    
}
/**
 *  3.加载完毕后调用(服务器的数据已经完全返回后)
 */
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    
}

通过执行下载操作,分别log上面三个方法,会发现didReceiveData这个方法会被频繁的调用,每次都会传回来一部分data,下面是官方api对这个方法的说明

is called with a single immutable NSData object to the delegate,
representing the next portion of the data loaded from the connection. This is the only guaranteed for the delegate to receive the data from the resource load.

由此我们可以知道,这种下载方式是通过这个代理方法每次传回来一部分文件,最终我们把每次传回来的数据合并成一个我们需要的文件。

这时候我们通常想到的方法是定义一个全局的NSMutableData,接受到响应的时候初始化这个MutableData,在didReceiveData方法里面去拼接
[self.totalData appendData:data];
最后在完成下载的方法里面吧整个MutableData写入沙盒。
代码如下:

@property (weak, nonatomic) IBOutlet UIProgressView *myPregress;

@property (nonatomic,strong) NSMutableData* fileData;

/**
 *  文件的总长度
 */
@property (nonatomic, assign) long long totalLength;
/**
 *  1.接收到服务器的响应就会调用
 *
 *  @param response   响应
 */
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    self.fileData = [NSMutableData data];
    // 获取要下载的文件的大小
    self.totalLength = response.expectedContentLength;
}
/**
 *  2.当接收到服务器返回的实体数据时调用(具体内容,这个方法可能会被调用多次)
 *
 *  @param data       这次返回的数据
 */
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    [self.fileData appendData:data];
    self.myPregress.progress = (double)self.fileData.length / self.totalLength;
}
/**
 *  3.加载完毕后调用(服务器的数据已经完全返回后)
 */
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    // 拼接文件路径
    NSString *cache = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    NSString *file = [cache stringByAppendingPathComponent:response.suggestedFilename];
    
    // 写到沙盒中
    [self.fileData writeToFile:file atomically:YES];
}

我这里下载的是javajdk。(度娘的地址)
注意:通常大文件下载是需要给用户展示下载进度的。
这个数值是 已经下载的数据大小/要下载的文件总大小
已经下载的数据我们可以记录,要下载的文件总大小在服务器返回的响应头里面可以拿到,在接受到响应的方法里执行

 NSHTTPURLResponse *res = (NSHTTPURLResponse*)response;
    
    NSDictionary *headerDic = res.allHeaderFields;
    NSLog(@"%@",headerDic);
    self.fileLength = [[headerDic objectForKey:@"Content-Length"] intValue];

不得不说苹果太为开发者考虑了,我们不必这么麻烦的去获取文件总大小了,
response.expectedContentLength 这句代码就搞定了。
response.suggestedFilename 这句代表获取下载的文件名


这里写图片描述

题外话扯的有点多,言归正传,这样我们确实可以下载文件,最后拿到的文件也能正常运行


这里写图片描述

但是有个致命的问题,内存!用来接受文件的NSMutableData一直都在内存中,会随着文件的下载一直变大,


这里写图片描述

所有这种处理方式绝对是不合理的。

合理的方式在我们获取一部分data的时候就写入沙盒中,然后释放内存中的data。

这里要用到NSFilehandle这个类,这个类可以实现对文件的读取、写入、更新。
下面总结了一些常用的NSFileHandle的方法,在这个表中,fh是一个NSFileHandle对象,data是一个NSData对象,path是一个NSString 对象,offset是易额Unsigned long long变量。


这里写图片描述

具体关于NSFileHandle的用法各位自行搜索。

在接受到响应的时候就在沙盒中创建一个空的文件,然后每次接收到数据的时候就拼接到这个文件的最后面,通过- (unsigned long long)seekToEndOfFile; 这个方法

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    // 文件路径
    NSString* ceches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    NSString* filepath = [ceches stringByAppendingPathComponent:response.suggestedFilename];
    
    // 创建一个空的文件到沙盒中
    NSFileManager* mgr = [NSFileManager defaultManager];
    [mgr createFileAtPath:filepath contents:nil attributes:nil];
    
    // 创建一个用来写数据的文件句柄对象
    self.writeHandle = [NSFileHandle fileHandleForWritingAtPath:filepath];
    
    // 获得文件的总大小
    self.totalLength = response.expectedContentLength;
                        
}
/**
 *  2.当接收到服务器返回的实体数据时调用(具体内容,这个方法可能会被调用多次)
 *
 *  @param data       这次返回的数据
 */
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    // 移动到文件的最后面
    [self.writeHandle seekToEndOfFile];
    
    // 将数据写入沙盒
    [self.writeHandle writeData:data];
    
    // 累计写入文件的长度
    self.currentLength += data.length;
    
    // 下载进度
    self.myPregress.progress = (double)self.currentLength / self.totalLength;
}
/**
 *  3.加载完毕后调用(服务器的数据已经完全返回后)
 */
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    self.currentLength = 0;
    self.totalLength = 0;
    
    // 关闭文件
    [self.writeHandle closeFile];
    self.writeHandle = nil;
}

这样在下载过程中内存就会一直很稳定了,并且下载的文件也是没问题的。


这里写图片描述
断点下载

暂停/继续下载也是现在下载中必备的功能了,如果没有暂停功能,用户体验相比会很差,而且如果突然网络不好中断了,没有实现断点下载的话只有重新下了。。。
下面让我们来加入断点下载功能吧。
NSURLConnection 只提供了一个cancel方法,这并不是暂停,而是取消下载任务。如果要实现断点下载必须要了解HTTP协议中请求头的Range。

这里写图片描述

不难看出,通过设置请求头的Range我们可以指定下载的位置、大小。
那么我们这样设置bytes=500- 从500字节以后的所有字节,
只需要在didReceiveData中记录已经写入沙盒中文件的大小(self.currentLength),
把这个大小设置到请求头中,因为第一次下载肯定是没有执行过didReceive方法,self.currentLength也就为0,也就是从头开始下。

注意:通过这种方法,我从我们公司的服务器上下载东西暂停开始都没用问题,下载好的视频也能正常播放,但是百度软件的下载链接不成功(暂停以后在请求服务器不给响应),应该是百度服务器做了限制吧,或者我写的不完美,有想法欢迎交流。

上代码:

#pragma mark --按钮点击事件

- (IBAction)btnClicked:(UIButton *)sender {
    
    // 状态取反
    sender.selected = !sender.isSelected;
    
    // 断点续传
    // 断点下载
    
    if (sender.selected) { // 继续(开始)下载
        // 1.URL
        NSURL *url = [NSURL URLWithString:@"http://localhost:8080//term_app/hdgg.zip"];
        
        // 2.请求
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
        
        // 设置请求头
        NSString *range = [NSString stringWithFormat:@"bytes=%lld-", self.currentLength];
        [request setValue:range forHTTPHeaderField:@"Range"];
        
        // 3.下载
        self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
    } else { // 暂停
        
        [self.connection cancel];
        self.connection = nil;
    }
}

在下载过程中,为了提高效率,充分利用cpu性能,通常会执行多线程下载,代码就不贴了,分析一下思路:

下载开始,创建一个和要下载的文件大小相同的文件(如果要下载的文件为100M,那么就在沙盒中创建一个100M的文件,然后计算每一段的下载量,开启多条线程下载各段的数据,分别写入对应的文件部分)。

NSURLSession下载方式

上面这种下载文件的方式确实比较复杂,要自己去控制内存写入相应的位置,不过在苹果在iOS7推出了一个新的类NSURLSession,它具备了NSURLConnection所具备的方法,同时也比它更强大。苹果推出它的目的大有取代NSURLConnection的趋势或者目的。

NSURLSession 也可以发送Get/Post请求,实现文件的下载和上传。
在NSURLSesiion中,任何请求都可以被看做是一个任务。其中有三种任务类型

// NSURLSessionDataTask : 普通的GET\POST请求
// NSURLSessionDownloadTask : 文件下载
// NSURLSessionUploadTask : 文件上传(很少用,一般服务器不支持)

NSURLSession 简单使用

NSURLSession发送请求非常简单,与connection不同的是,任务创建后不会自动发送请求,需要手动开始执行任务。

 // 1.得到session对象
    NSURLSession* session = [NSURLSession sharedSession];
    NSURL* url = [NSURL URLWithString:@""];
    
    // 2.创建一个task,任务
    NSURLSessionDataTask* dataTask = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        // data 为返回数据
    }];
    
    // 3.开始任务
    [dataTask resume];
// 发送post请求 自定义请求头
[session dataTaskWithRequest:<#(NSURLRequest *)#> completionHandler:<#^(NSData *data, NSURLResponse *response, NSError *error)completionHandler#>]
NSURLSession 下载

使用NSURLSession就非常简单了,不需要去考虑什么边下载边写入沙盒的问题,苹果都帮我们做好了。代码如下

 NSURL* url = [NSURL URLWithString:@"http://dlsw.baidu.com/sw-search-sp/soft/9d/25765/sogou_mac_32c_V3.2.0.1437101586.dmg"];
    
    // 得到session对象
    NSURLSession* session = [NSURLSession sharedSession];
    
    // 创建任务
    NSURLSessionDownloadTask* downloadTask = [session downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
        
    }];
    // 开始任务
    [downloadTask resume];

是不是跟NSURLConnection很像,但仔细看会发现回调的方法里面并没用NSData传回来,多了一个location,顾名思义,location就是下载好的文件写入沙盒的地址,打印一下发现下载好的文件被自动写入的temp文件夹下面了。

location:file:///Users/yeaodong/Library/Developer/CoreSimulator/Devices/E52B4B95-53E1-46A2-9881-8C969958FBC0/data/Containers/Data/Application/BFB9F0CA-0F50-4682-BBBD-B71B54C39EBE/tmp/CFNetworkDownload_YNnuIS.tmp

这里写图片描述

不过在下载完成之后会自动删除temp中的文件,所有我们需要做的只是在回调中把文件移动(或者复制,反正之后会自动删除)到caches中。

NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
        // response.suggestedFilename : 建议使用的文件名,一般跟服务器端的文件名一致
        NSString *file = [caches stringByAppendingPathComponent:response.suggestedFilename];
        
        // 将临时文件剪切或者复制Caches文件夹
        NSFileManager *mgr = [NSFileManager defaultManager];
        
        // AtPath : 剪切前的文件路径
        // ToPath : 剪切后的文件路径
        [mgr moveItemAtPath:location.path toPath:file error:nil];

不过通过这种方式下载有个缺点就是无法监听下载进度,要监听下载进度,苹果通常的作法是通过delegate,这里也一样。而且NSURLSession的创建方式也有所不同。
首先遵守协议<NSURLSessionDownloadDelegate> 注意不要写错
点进去发现协议里面有三个方法。

#pragma mark -- NSURLSessionDownloadDelegate
/**
 *  下载完毕会调用
 *
 *  @param location     文件临时地址
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
}
/**
 *  每次写入沙盒完毕调用
 *  在这里面监听下载进度,totalBytesWritten/totalBytesExpectedToWrite
 *
 *  @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
{
    self.pgLabel.text = [NSString stringWithFormat:@"下载进度:%f",(double)totalBytesWritten/totalBytesExpectedToWrite];
}

/**
 *  恢复下载后调用,
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
 didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes
{
    
}

这上面的注释已经很详细了,相信大家都能看懂吧。

NSURLSession创建方式,这里就不能使用Block回调方式了,如果给下载任务设置了completionHandler这个block,也实现了下载的代理方法,优先执行block,代理方法也就不会执行了。

// 得到session对象
    NSURLSessionConfiguration* cfg = [NSURLSessionConfiguration defaultSessionConfiguration]; // 默认配置
    
    NSURLSession* session = [NSURLSession sessionWithConfiguration:cfg delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    
    // 创建任务
    NSURLSessionDownloadTask* downloadTask = [session downloadTaskWithURL:url];
    
    // 开始任务
    [downloadTask resume];

相比之前的NSURLConnection方式简单很多吧,用NSURLSessionDownloadTask做断点下载也很简单,我们先了解一下任务的取消方法

- (void)cancelByProducingResumeData:(void (^)(NSData *resumeData))completionHandler;

取消操作以后会调用一个Block,并传入一个resumeData,该参数包含了继续下载文件的位置信息。也就是说,当你下载了10M得文件数据,暂停了。那么你下次继续下载的时候是从第10M这个位置开始的,而不是从文件最开始的位置开始下载。因而为了保存这些信息,所以才定义了这个NSData类型的这个属性:resumeData。这个data只包含了url跟已经下载了多少数据,不会很大,不用担心内存问题。

另外,session还提供了通过resumeData来创建任务的方法

- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData;

我们只需要在取消操作的回调中记录好resumeData,然后在恢复下载的适合通过上面的方法创建任务就好了,相比NSURLconnection简单太多了。
需要注意的是Block中循环引用的问题

__weak typeof(self) selfVc = self;
    [self.downloadTask cancelByProducingResumeData:^(NSData *resumeData) {
        selfVc.resumeData = resumeData;
        selfVc.downloadTask = nil;
    }];

示例程序下载:https://github.com/hongfenglt/HFDownLoad

这篇博客断断续续写了两三天,可能某些地方思路有些乱,欢迎大神指正。

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

推荐阅读更多精彩内容