实现思路:
在下载文件的时候不再是整块的从头开始下载,而是看当前文件已经下载到哪个地方,然后从该地方接着往后面下载。可以通过在请求对象中设置请求头实现。
解决方案:
//2.创建请求对象
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
//2.1 设置下载文件的某一部分
// 只要设置HTTP请求头的Range属性, 就可以实现从指定位置开始下载
/*
表示头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.currentLength];
[request setValue:range forHTTPHeaderField:@"Range"];
注意点: 下载进度并判断是否需要重新创建文件
//获得当前要下载文件的总大小(通过响应头得到)
NSHTTPURLResponse *res = (NSHTTPURLResponse *)response;
//注意点:res.expectedContentLength获得是本次请求要下载的文件的大小(并非是完整的文件的大小)
//因此:文件的总大小 == 本次要下载的文件大小+已经下载的文件的大小
self.totalLength = res.expectedContentLength + self.currentLength;
NSLog(@"----------------------------%zd",self.totalLength);
//0 判断当前是否已经下载过,如果当前文件已经存在,那么直接返回
if (self.currentLength >0) {
return;
}
代码:
//
// Created by 朝阳 on 2017/12/11.
// Copyright © 2017年 sunny. All rights reserved.
//
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, assign) NSInteger totalSize;
@property (nonatomic, assign) NSInteger currentSize;
@property (weak, nonatomic) IBOutlet UISlider *slider;
/** 沙盒路径 */
@property (nonatomic,strong) NSString *fullPath;
/** 文件句柄*/
@property (nonatomic, strong)NSFileHandle *handle;
/** 连接对象 */
@property (nonatomic, strong) NSURLConnection *connect;
@end
@implementation ViewController
- (IBAction)startDownload:(id)sender
{
[self download];
}
- (IBAction)cancelDownload:(id)sender
{
[self.connect cancel];
}
- (IBAction)goOnDownload:(id)sender
{
[self download];
}
// 内容飙升的原因: self.fileData是一个变量. 把从网络中的数据保存到了fileData中,并不会释放
// self.fileData 在写入到沙盒中, 解决内存飙升问题: 直接把数据写入到沙盒中,不通过fileData
- (void)download
{
//1. url
NSURL *url = [NSURL URLWithString:@"http://localhost:8080/MJServer/resources/videos/minion_01.mp4"];
//2. 创建请求对象
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
//设置请求头信息,告诉服务器值请求一部分数据range
/*
bytes=0-100
bytes=-100
bytes=0- 请求100之后的所有数据
*/
// 从文件的部分开始下载
NSString *range = [NSString stringWithFormat:@"bytes=%ld-",self.currentSize];
[request setValue:range forHTTPHeaderField:@"Range"];
//3. 发送请求(代理)
self.connect = [[NSURLConnection alloc] initWithRequest:request delegate:self];
}
#pragma -mark NSURLConnectionDataDelegate
// 当接收到服务器响应的时候调用
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
// self.totalSize放在 if判断前
//1. 得到 文件的总大小(本次请求的文件数据的总大小)
// 本次请求的文件数据大小 != 文件总大小(如果再次发送请求的时候,此时的self.totalSize 就小于文件的大小,因此在\
在后面计算 1.0 * self.currentSize / self.totalSize 的时候,会出现数据错乱) 因此要加上当前已经下载的数据.
self.totalSize = response.expectedContentLength + self.currentSize;
// 当self.currentSize已经下载部分数据后,就不继续往后走了.
// 不作判断的话, 就会继续创建一个空文件, 然后后面的数据就下载到另一个空文件去了.
if (self.currentSize > 0) {
return;
}
NSLog(@"didReceiveResponse");
// self.totalSize 放在if判断后.(此时只发送一次请求,所有self.totalSize就是文件的大小)
//1. 得到 文件的总大小(本次请求的文件数据的总大小)
// self.totalSize = response.expectedContentLength;
//2. 写数据到沙盒中
self.fullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"1.mp4"];
//3. 创建一个空文件
[[NSFileManager defaultManager] createFileAtPath:self.fullPath contents:nil attributes:nil];
//4. 创建文件句柄(指针)
self.handle = [NSFileHandle fileHandleForWritingAtPath:self.fullPath];
}
// 当接收到服务器返回数据的时候调用-并调用多次(数据是一点点返回的)
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
// 这样直接把数据 写入到沙盒路径中,是不正确的. 会把下载的前面下载的data 给 覆盖掉
//[data writeToFile:self.fullPath atomically:YES];
//1. 移动文件句柄到每次data的末尾
[self.handle seekToEndOfFile];
//2. 写数据
[self.handle writeData:data];
//3. 获得进度
self.currentSize += data.length;
// 下载进度 = 已经下载 / self.totalSize
self.slider.value = 1.0 * self.currentSize / self.totalSize;
NSLog(@"%f",1.0 * self.currentSize / self.totalSize);
}
// 当发送请求失败的时候调用
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
NSLog(@"%@",error);
}
// 当发送请求完成后调用
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
//1. 关闭文件句柄
[self.handle closeFile];
self.handle = nil;
NSLog(@"connectionDidFinishloading");
NSLog(@"%@",self.fullPath);
}
@end
使用输出流也可以实现NSFileHandle的功能
// Created by 朝阳 on 2017/12/11.
// Copyright © 2017年 sunny. All rights reserved.
//
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, assign) NSInteger totalSize;
@property (nonatomic, assign) NSInteger currentSize;
@property (weak, nonatomic) IBOutlet UISlider *slider;
/** 沙盒路径 */
@property (nonatomic,strong) NSString *fullPath;
/** 连接对象 */
@property (nonatomic, strong) NSURLConnection *connect;
/* 输出流对象 */
@property (nonatomic, strong) NSOutputStream *stream;
@end
@implementation ViewController
- (IBAction)startDownload:(id)sender
{
[self download];
}
- (IBAction)cancelDownload:(id)sender
{
[self.connect cancel];
}
- (IBAction)goOnDownload:(id)sender
{
[self download];
}
// 内容飙升的原因: self.fileData是一个变量. 把从网络中的数据保存到了fileData中,并不会释放
// self.fileData 在写入到沙盒中, 解决内存飙升问题: 直接把数据写入到沙盒中,不通过fileData
- (void)download
{
//1. url
NSURL *url = [NSURL URLWithString:@"http://flv2.bn.netease.com/videolib3/1604/28/fVobI0704/SD/fVobI0704-mobile.mp4"];
//2. 创建请求对象
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
//设置请求头信息,告诉服务器值请求一部分数据range
/*
bytes=0-100
bytes=-100
bytes=0- 请求100之后的所有数据
*/
// 从文件的部分开始下载
NSString *range = [NSString stringWithFormat:@"bytes=%ld-",self.currentSize];
[request setValue:range forHTTPHeaderField:@"Range"];
//3. 发送请求(代理)
self.connect = [[NSURLConnection alloc] initWithRequest:request delegate:self];
}
#pragma -mark NSURLConnectionDataDelegate
// 当接收到服务器响应的时候调用
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
NSLog(@"didReceiveResponse");
self.totalSize = response.expectedContentLength + self.currentSize;
// 当self.currentSize已经下载部分数据后,就不继续往后走了.
// 不作判断的话, 就会继续创建一个空文件, 然后后面的数据就下载到另一个空文件去了.
if (self.currentSize > 0) {
return;
}
//2. 写数据到沙盒中
// response.suggestedFilename 获取资源的名称
self.fullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:response.suggestedFilename];
//3. 创建输出流
/*
param1: 文件的路径
param2: YES 追加
特点: 如果该输出流指向的地址没有文件,那么会自动创建一个空的文件
*/
NSOutputStream *stream = [[NSOutputStream alloc] initToFileAtPath:self.fullPath append:YES];
// 开启输出流
[stream open];
self.stream = stream;
}
// 当接收到服务器返回数据的时候调用-并调用多次(数据是一点点返回的)
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
// 写数据
[self.stream write:data.bytes maxLength:data.length];
// 获得进度
self.currentSize += data.length;
// 下载进度 = 已经下载 / self.totalSize
self.slider.value = 1.0 * self.currentSize / self.totalSize;
NSLog(@"%f",1.0 * self.currentSize / self.totalSize);
}
// 当发送请求失败的时候调用
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
NSLog(@"%@",error);
}
// 当发送请求完成后调用
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
//关闭流
[self.stream close];
self.stream = nil;
NSLog(@"connectionDidFinishloading");
NSLog(@"%@",self.fullPath);
}
@end
使用多线程下载文件思路
01 开启多条线程,每条线程都只下载文件的一部分(通过设置请求头中的Range来实现)
02 创建一个和需要下载文件大小一致的文件,判断当前是那个线程,根据当前的线程来判断下载的数据应该写入到文件中的哪个位置。(假设开5条线程来下载10M的文件,那么线程1下载0-2M,线程2下载2-4M一次类推,当接收到服务器返回的数据之后应该先判断当前线程是哪个线程,假如当前线程是线程2,那么在写数据的时候就从文件的2M位置开始写入)
03 代码相关:使用NSFileHandle这个类的seekToFileOfSet方法,来向文件中特定的位置写入数据。
04 技术相关
a.每个线程通过设置请求头下载文件中的某一个部分
b.通过NSFileHandle向文件中的指定位置写数据