iOS之[文件下载 / 大文件下载 / 断点下载]

援引:https://my.oschina.net/u/2462423/blog/602519

1.NSData(小文件下载)

[NSData dataWithContentsOfURL:] 就是一种文件下载方式,Get请求
但是这种下载方式需要放到子线程中
    NSURL *url = [NSURL URLWithString:@"http://10.167.20.151:8080/Admin/help/test.png"];
    NSData *data = [NSData dataWithContentsOfURL:url];
2.NSURLConnection

2.1小文件下载(NSURLConnection)

通过NSURLConnection发送一个异步的Get请求,一次性将整个文件返回
    NSURL* url = [NSURL URLWithString:@"http://10.167.20.151:8080/Admin/help/test.png"];

    [NSURLConnection 
        sendAsynchronousRequest:[NSURLRequest requestWithURL:url] 
                          queue:[NSOperationQueue mainQueue] 
              completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {

                    //atomically:原子性
                    [data writeToFile:filePath atomically:YES];      
                }];    
2.2 大文件下载(NSURLConnection)

大文件下载不能一次性返回整个文件,否则会造成内存泄漏,系统崩溃
// 发送请求去下载 (创建完conn对象后,会自动发起一个异步请求)
IOS9已经废弃:[NSURLConnection connectionWithRequest:request delegate:self];
需要实现NSURLConnectionDataDelegate 协议,常用的几个如下:
/**
 *  请求失败时调用(请求超时、网络异常)
 */
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;

/**
 *  1.接收到服务器的响应就会调用
 */
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;

/**
 *  2.当接收到服务器返回的实体数据时调用(具体内容,这个方法可能会被调用多次)
 */
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;

/**
 *  3.加载完毕后调用(服务器的数据已经完全返回后)
 */
- (void)connectionDidFinishLoading:(NSURLConnection *)connection;
didReceiveData方法会被频繁的调用,每次都会传回来一部分data
最终我们把每次传回来的数据合并成一个我们需要的文件。
通常合并文件是定义一个全局的NSMutableData,通过[mutableData appendData:data];来合并,最后将Data写入沙盒
代码如下:
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;

@property (nonatomic, strong) NSURLRequest *request;
@property (nonatomic, strong) NSURLResponse *response;

@property (nonatomic, strong) NSMutableData *fileData;

@property (nonatomic, assign) long long fileLength; //>>文件长度
@property (nonatomic, strong) NSString *fileName;   //>>文件名
- (void)viewDidLoad {
    [super viewDidLoad];
    
    _request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://10.167.20.151:8080/Admin/help/objective-c.pdf"]];
    //IOS9.0已经被废弃
    [NSURLConnection connectionWithRequest:_request delegate:self];
}

/**
 *接收到服务器的响应
 */
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{

    self.response = response;
    self.fileData = [NSMutableData data];
    
    //获取下载文件大小
    self.fileLength = response.expectedContentLength;
    //获取文件名
    self.fileName = response.suggestedFilename;
}

/**
 *接收到服务器返回的数据(可能被调用多次)
 */
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{

    [self.fileData appendData:data];
    
    //更新画面中的进度
    double progress = (double)self.fileData.length/self.fileLength;
    self.progressView.progress = progress;
}

/**
 *服务器返回数据完了
 */
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{

    NSString *cache = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    NSString *filePath = [cache stringByAppendingPathComponent:_fileName];
    
    [self.fileData writeToFile:filePath atomically:YES];
}
但是有个致命的问题,内存!用来接受文件的NSMutableData一直都在内存中,会随着文件的下载一直变大,
内存

合理的方式在我们获取一部分data的时候就写入沙盒中,然后释放内存中的data
要用到NSFilehandle这个类,这个类可以实现对文件的读取、写入、更新
每次接收到数据的时候就拼接文件后面,通过- (unsigned long long)seekToEndOfFile;方法
/**
 *接收到服务器的响应
 */
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
    // 文件路径
    NSString* caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    NSString* filepath = [ceches stringByAppendingPathComponent:response.suggestedFilename];

    // 创建一个空的文件到沙盒中
    NSFileManager* fileManager = [NSFileManager defaultManager];
    [fileManager createFileAtPath:filepath contents:nil attributes:nil];

    // 创建一个用来写数据的文件句柄对象:用来填充数据
    self.writeHandle = [NSFileHandle fileHandleForWritingAtPath:filepath];

    // 获得文件的总大小
    self.fileLength = response.expectedContentLength;

}
/**
 *  2.当接收到服务器返回的实体数据时调用(具体内容,这个方法可能会被调用多次)
 */
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    // 移动到文件的最后面
    [self.writeHandle seekToEndOfFile];

    // 将数据写入沙盒
    [self.writeHandle writeData:data];

    // 累计写入文件的长度
    self.currentLength += data.length;

    // 下载进度
    self.pregressView.progress = (double)self.currentLength / self.fileLength;
}
/**
 *  3.加载完毕后调用(服务器的数据已经完全返回后)
 */
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    self.currentLength = 0;
    self.fileLength = 0;

    // 关闭文件
    [self.writeHandle closeFile];
    self.writeHandle = nil;
}
下载过程中内存就会一直很稳定了,并且下载的文件也是没问题的
内存正常

2.3 断点下载(NSURLConnection)

断点续传的response状态码为206
暂停/继续下载也是现在下载中必备的功能
NSURLConnection 只提供了一个cancel方法,这并不是暂停,而是取消下载任务。如果要实现断点下载必须要了解HTTP协议中请求头的Range
通过设置请求头的Range我们可以指定下载的位置、大小
Range实例:

bytes = 0-499           从0到499的头500个字节

bytes = 500-999         从500到999的第二个500个字节

bytes = 500-            500之后的所有字节

bytes = -500            最后500个字节

bytes = 0-599,700-899   同时指定多个范围
||


pragma mark --按钮点击事件,

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

    // 状态取反
    sender.selected = !sender.isSelected;

    // 断点下载

    if (sender.selected) { // 继续(开始)下载
        // 1.URL
        NSURL *url = [NSURL URLWithString:@"http://10.167.20.151:8080/Admin/help/Code.pdf"];

        // 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;
    }
}
2.4 断点下载的全部代码

@interface NSURLConnectionViewController ()<NSURLConnectionDataDelegate>

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

@property (nonatomic, strong) NSURL *url;
@property (nonatomic, strong) NSURLRequest *request;
@property (nonatomic, strong) NSHTTPURLResponse *response;
@property (nonatomic, strong) NSURLConnection *connection;

@property (nonatomic, strong) NSFileHandle *fileHandle;

@property (nonatomic, assign) long long currentLength; //>>写入文件的长度
@property (nonatomic, assign) long long fileLength; //>>文件长度
@property (nonatomic, strong) NSString *fileName;   //>>文件名

@end

@implementation NSURLConnectionViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    NSLog(@"%@", NSHomeDirectory());
    self.url = [NSURL URLWithString:@"http://10.167.20.151:8080/AdminConsole/help/objective-c.pdf"];
}

#pragma mark - NSURLConnectionDataDelegate

/**
 *请求失败
 */
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{

    NSLog(@"error");
}
/**
 *接收到服务器的响应
 */
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{

    self.response = (NSHTTPURLResponse *)response;
    
    if (self.response.statusCode == 206) {//!!!断点续传的状态码为206
        
        if (self.currentLength) {
            return;
        }
        
        //获取下载文件大小
        self.fileLength = response.expectedContentLength;

        //获取文件名
        self.fileName = response.suggestedFilename;
        
        //文件路径
        NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];

        NSString *filePath = [caches stringByAppendingPathComponent:_fileName];
        
        //创建一个空的文件到沙盒
        NSFileManager *fileManager = [NSFileManager defaultManager];
        [fileManager createFileAtPath:filePath contents:nil attributes:nil];
        
        //创建一个用来写数据的文件句柄
        self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:filePath];
    }else{
    
        [self.connection cancel];
        self.connection = nil;
        NSLog(@"该文件不存在");
    }
    
}

/**
 *接收到服务器返回的数据(可能被调用多次)
 */
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
        
        //移动到文件末尾
        [self.fileHandle seekToEndOfFile];
        
        //写入数据到文件
        [self.fileHandle writeData:data];
        
        self.currentLength += data.length;
        
        //更新画面中的进度

        double progress = (double)self.currentLength/self.fileLength;
        self.progressView.progress = progress;
}

/**
 *服务器返回数据完了
 */
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
     
        self.currentLength = 0;
        self.fileLength = 0;
        [self.fileHandle closeFile];
        self.fileHandle = nil;
}

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

    //暂停<->开始转换
    sender.selected = !sender.isSelected;
    
    if (sender.selected) {//开始下载
        
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:_url];
        
        //设置请求头(GET)
        NSString *range = [NSString stringWithFormat:@"bytes=%lld-", self.currentLength];
        [request setValue:range forHTTPHeaderField:@"Range"];
        
        self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
    }else{ //暂停
    
        [self.connection cancel];
        self.connection = nil;
    }
}
@end
在下载过程中,为了提高效率,充分利用cpu性能,通常会执行多线程下载。。。待更新!!!
4. NSURLSession

生命周期的两种方式-1:系统代理-Block方式(流程简单-优先)
          -2:指定代理类:delegate(流程复杂)
上面这种下载文件的方式确实比较复杂,要自己去控制内存写入相应的位置
iOS7推出了一个新的类NSURLSession,它具备了NSURLConnection所具备的方法,同时也比它更强大
NSURLSession 也可以发送Get/Post请求,实现文件的下载和上传。
在NSURLSesiion中,任何请求都可以被看做是一个任务。其中有三种任务类型
    NSURLSessionDataTask : 普通的GET\POST请求
    NSURLSessionDownloadTask : 文件下载
    NSURLSessionUploadTask : 文件上传(很少用,一般服务器不支持)
4.1 NSURLSession 简单使用

NSURLSession发送请求非常简单,与connection不同的是,任务创建后不会自动发送请求,需要手动开始执行任务
    // 1.得到session对象
    NSURLSession* session = [NSURLSession sharedSession];
    NSURL* url = [NSURL URLWithString:@""];

    // 2.创建一个task,任务(GET)
    NSURLSessionDataTask* dataTask = 
             [session dataTaskWithURL:url 
                    completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

        // data 为返回数据
    }];

    // 3.开始任务
    [dataTask resume];
POST请求:可以自定义请求头
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSData *data, NSURLResponse *response, NSError *error))completionHandler;
4.2 NSURLSession文件下载

使用NSURLSession就非常简单了,不需要去考虑什么边下载边写入沙盒的问题,苹果都帮我们做好了
只用将下载好的文件通过NSFileManager剪切到指定位置
需要用到NSURLSession的子类:NSURLSessionDownloadTask
    NSURL* url = [NSURL URLWithString:@"http://10.167.20.151:8080/Admin/help/test.png"];

    // 得到session对象
    NSURLSession* session = [NSURLSession sharedSession];

    // 创建任务
    NSURLSessionDownloadTask* downloadTask = [session downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {

        //将文件迁移到指定路径下
        NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];

        NSString *file = [caches stringByAppendingPathComponent:response.suggestedFilename];

        // 将临时文件剪切或者复制Caches文件夹
        NSFileManager *fileManager = [NSFileManager defaultManager];

        // AtPath : 剪切前的文件路径
        // ToPath : 剪切后的文件路径
        [fileManager moveItemAtPath:location.path toPath:file error:nil];
    }];

    // 开始任务
    [downloadTask resume];
location就是下载好的文件写入沙盒的地址
该方式无法监听下载进度
若要监听进度需要实现< NSURLSessionDownloadDelegate >协议,不能使用Block方式
/**
 *  下载完毕会调用
 */
- (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
{...}

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

:fa-font:1.暂停下载

resumeData,该参数包含了继续下载文件的位置信息
resumeData只包含了url跟已经下载了多少数据,不会很大,不用担心内存问题
- (void)cancelByProducingResumeData:(void (^)(NSData *resumeData))completionHandler;
!!!需要注意的是Block中循环引用的问题

    __weak typeof(self) selfVc = self;
    [self.downloadTask cancelByProducingResumeData:^(NSData *resumeData) {
        selfVc.resumeData = resumeData;
        selfVc.downloadTask = nil;
    }];
:fa-bold:2.恢复下载

- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData;
4.3 NSURLSession断点下载所有代码

@interface NSURLSessionController () <NSURLSessionDownloadDelegate>

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

//下载任务
@property (nonatomic, strong) NSURLSessionDownloadTask* downloadTask;

//resumeData记录下载位置
@property (nonatomic, strong) NSData* resumeData;

@property (nonatomic, strong) NSURLSession* session;

@end

@implementation NSURLSessionController
/**
 *  session的懒加载
 */
- (NSURLSession *)session
{
    if (nil == _session) {
        
        NSURLSessionConfiguration *cfg = [NSURLSessionConfiguration defaultSessionConfiguration];
        self.session = [NSURLSession sessionWithConfiguration:cfg delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    }
    return _session;
}

- (void)viewDidLoad {
    [super viewDidLoad];
 
}
/**
 *  从0开始下载
 */
- (void)startDownload
{

    NSURL* url = [NSURL URLWithString:@"http://10.167.20.151:8080/Admin/help/objective-c.pdf"];
    
    // 创建任务
    self.downloadTask = [self.session downloadTaskWithURL:url];

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

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

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

#pragma mark -- NSURLSessionDownloadDelegate
/**
 *  下载完毕会调用
 *
 *  @param location     文件临时地址
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location
{
    //文件路径
    NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
    NSString *file = [caches stringByAppendingPathComponent:downloadTask.response.suggestedFilename];
    
    // 将临时文件剪切或者复制Caches文件夹
    NSFileManager *fileManager = [NSFileManager defaultManager];
    
    // AtPath : 剪切前的文件路径
    // ToPath : 剪切后的文件路径
    [fileManager moveItemAtPath:location.path toPath:file error:nil];
    
    NSLog(@"下载完成");
}

/**
 *  每次写入沙盒完毕调用
 *  在这里面监听下载进度,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.myPregress.progress = (double)totalBytesWritten/totalBytesExpectedToWrite;
}

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

#pragma mark --按钮点击事件

- (IBAction)btnClicked:(UIButton *)sender {
    
    // 按钮状态取反
    sender.selected = !sender.isSelected;
    
    if (nil == self.downloadTask) { // 开始(继续)下载
        if (self.resumeData) { // 继续下载
            [self resume];
        }else{ // 从0开始下载
            [self startDownload];
        }
        
    }else{ // 暂停
        [self pause];
    }
    
}

 

posted on 2019-05-06 11:44  高彰  阅读(1459)  评论(0编辑  收藏  举报

导航