NSURLSession实现文件下载

NSURLSession实现文件下载_第1张图片
30-02.png

  • NSURLSessionTask
常见方法
- (void)suspend; // 暂停
- (void)resume; // 恢复
- (void)cancel; // 取消
@property (readonly, copy) NSError *error; // 错误
@property (readonly, copy) NSURLResponse *response; // 响应

文件下载的方式4种方式

方式一:Block+delegate

import "ViewController.h"

@interface ViewController ()


@end

@implementation ViewController


-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self downloadFileDataWithBlock];
}

//方法1
//特点:能够直接把文件下载到沙盒中,我们需要做文件剪切处理(不会有内存飙升的问题)
//缺点:无法监听文件的下载进度
//应用:小文件下载
-(void)downloadFileDataWithBlock
{
    //1.确定URL
    NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_01.mp4"];
    
    //2.创建可变的请求对象
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    
    //3.创建session
    NSURLSession *session = [NSURLSession sharedSession];
    
    //4.创建task
    /*
     第一个参数:请求对象
     第二个参数:completionHandler 当下载结束(成功|失败)的时候调用
                location:位置,该文件保存到沙盒中的位置
                response:响应头信息
                error:错误信息
     */
    NSURLSessionDownloadTask *downloadTask = [session downloadTaskWithRequest:request completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        
        NSLog(@"%@",location);
        
        //6.处理文件
        //6.1 获得文件的名称
        NSString *fileName = response.suggestedFilename;
        
        //6.2 写路径到磁盘+拼接文件的全路径    
        NSString *fullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:fileName];

        //6.3 执行剪切操作
        [[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:fullPath] error:nil];
        
        NSLog(@"%@",fullPath);
        
    }];
    
    //5.执行task
    [downloadTask resume];
}

@end

运行结果:
NSURLSession实现文件下载_第2张图片
30-10.png

方式二:downloadTask+delegate


  • 优点:在取消按钮方法的内部,写上cancelByProducingResumeData方法可以实现断点下载
  • 缺点:他的代理方法无法实现离线下载。必须通过dataTask才可以实现离线下载.

4个前提条件:

1.控制器遵守协议:

NSURLSessionDownloadDelegate

2.控制器成为NSURLSession的代理:

NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];

3.控制器实现代理方法:

// 写数据的时候调用
- (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;

// 当下载完成时调用
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
                              didFinishDownloadingToURL:(NSURL *)location;

// 当整个请求结束的时候调用
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                           didCompleteWithError:(nullable NSError *)error;

4.创建下载任务对象:

// 创建一个有回调的task对象
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURL * __nullable location, NSURLResponse * __nullable response, NSError * __nullable error))completionHandler;
// 创建一个无回调的task对象
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request;

代码演示:

#import "ViewController.h"

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIProgressView *progerssView;
/** 下载任务*/
@property (nonatomic ,strong)  NSURLSessionDownloadTask *downloadTask;
/** 恢复下载的数据*/
@property (nonatomic ,strong) NSData *resumeData;
/** 会话对象*/
@property (nonatomic ,strong) NSURLSession *session;
@end

@implementation ViewController

-(void)viewDidLoad
{
    [super viewDidLoad];
    
    //监听系统推出
    //[cancelBtnClick];
    //resumeData写磁盘
}
#pragma mark --------------------
#pragma mark lazy loading
-(NSURLSession *)session
{
    if (_session == nil) {
        //让控制器成为NSURLSession的代理,代理方法在主队列,也就是主线程中执行
        _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    }
    return _session;
}

- (IBAction)startBtnClick:(id)sender
{
    [self downloadFileDataWithDelegate];
}

- (IBAction)suspendBtnClick:(id)sender
{
    //暂停任务
    [self.downloadTask suspend];
    NSLog(@"暂停任务----------");
}

- (IBAction)cancelBtnClick:(id)sender
{
    //取消任务
    //cancel :是不可以恢复
    //cancelByProducingResumeData:可以恢复的。就是传说中的断点续传
    
    [self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
        self.resumeData = resumeData;
    }];
    NSLog(@"取消任务_______________");
}

- (IBAction)goonBtnClick:(id)sender
{
    //先判断用户当前是处于暂停状态还是处于取消
    if (self.resumeData) {
        //用resumeData记录之前下载文件的最后一个结点,创建一个新的下载任务,从这个结点继续下载,并用downloadTask保存这个下载任务
        
        //session是NSURLSessionDownloadTask类型的对象  downloadTaskWithResumeData是返回值为NSURLSessionDownloadTask类型的方法,所以可以用对象调用这个方法
        self.downloadTask = [self.session downloadTaskWithResumeData:self.resumeData];
    }

    //继续下载
    [self.downloadTask resume];
    NSLog(@"继续下载++++++++++++");
}


//方法2:
//特点:能够直接把文件下载到沙盒中,我们需要做文件剪切处理(不会有内存飙升的问题)
//应用:通用
-(void)downloadFileDataWithDelegate
{
    //1.确定URL
    NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_01.mp4"];
    
    //2.创建可变的请求对象
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    
    //在懒加载的方法中创建session
    //3.创建session
    //NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    
    //创建下载任务对象 downloadTask. 通过调用downloadTask对象的resume、suspend、cancel方法,可以实现下载任务的开始,暂停和取消
    NSURLSessionDownloadTask *downloadTask = [self.session downloadTaskWithRequest:request];//懒加载self.session
    
    //5.执行task
    [downloadTask resume];
    
    //让downloadTask在当前类中的所有方法都可以使用,并且所有方法拿到的downloadTask都是实时的状态,而不是初始的状态
    self.downloadTask = downloadTask;//downloadTask是NSURLSessionDownloadTask类型
}

#pragma mark --------------------
#pragma mark NSURLSessionDownloadDelegate
/**
 *  1.写数据的时候调用
 *  @param bytesWritten              本次下载数据的大小
 *  @param totalBytesWritten         当前已经下载数据的总大小
 *  @param totalBytesExpectedToWrite 文件的总大小
 */
//didWriteData
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
    CGFloat progress = 1.0 * totalBytesWritten / totalBytesExpectedToWrite;
    NSLog(@"当前下载进度:%f",progress);
    
    //把当前的进度值赋给关联的进度条
    self.progerssView.progress = progress;
}
/**
 *  恢复下载的时候调用
 *
 *  @param fileOffset         当前从什么位置开始下载
 *  @param expectedTotalBytes 文件的总大小
 */
//didResumeAtOffset
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes
{
    NSLog(@"恢复下载---didResumeAtOffset");
}

/**
 * 当下载完成调用
 *
 *  @param location     文件的临时存储路径(磁盘/tmp)
 */
//didFinishDownloadingToURL
-(void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
    NSLog(@"%@",location);
    
    //1 获得文件的名称(要下载的文件的文件名称) 
    //suggestedFilename是系统推荐的名字,就是URL后面的minion_01.mp4
    NSString *fileName = downloadTask.response.suggestedFilename;
    
    //2 拼接文件的全路径 这个方法获取到的路径是Caches文件夹所在的路径。然后把下载的fileName文件放在Caches文件夹下.如果你想自定义文件的名字,可以把步骤1注释,把2的fileName替换为@"haha.mp4"
    NSString *fullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:fileName];
    
    /*
     Caches文件夹所在的路径为:
    
     /Users/zhangbin/Library/Developer/CoreSimulator/Devices/B6F42F9C-4DCD-4835-B79D-B0F68D274E8E/data/Containers/Data/Application/8E46EBB2-0375-4729-AF92-5D5C4ADDC20A/Library/Caches
     */
    
    //3 执行剪切操作  把location路径中的文件 剪切到 fullPath路径中
    [[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:fullPath] error:nil];
    
    NSLog(@"%@",fullPath);
}

/**
 *  当整个请求结束的时候调用
 *
 *  @param error   错误信息
 */
//didCompleteWithError
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    NSLog(@"didCompleteWithError");
}
@end

截图:

31-11.gif

方式三:dataTask+delegate+NSFileHandle(文件句柄)


  • 优点:
    • 1.在取消按钮方法的内部,不需要写cancelByProducingResumeData方法,手写代码可以实现断点下载
    • 2.代理方法可以实现离线下载.通过创建文件句柄+移动文件句柄(必不可少)
    • 3.一切皆因为dataTask代理方法可以接收服务器返回的数据(核心理解)
  • 对优点1的详细解释:
    • a:点击取消按钮,取消按钮方法执行[self.dataTask cancel];并将self.dataTask通过赋为nil来释放它
    • b:点击继续按钮,继续按钮方法执行[self.dataTask resume];self.dataTask为nil,所以会调用懒加载方法
    • c:懒加载方法中设置了请求对象,所以可以进行下载操作
    • d:懒加载方法中设置了请求范围,所以并不会从头开始下载.而是继续之前的进度下载

4个前提条件:

1.控制器遵守协议

NSURLSessionDataDelegate

2.控制器成为代理

NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];

3.控制器实现代理方法

// 接收到响应。主要功能:读取到要下载文件的真实大小
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
                                 didReceiveResponse:(NSURLResponse *)response
                                  completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler;

// 接收到数据。主要功能:累加已经下载的文件的大小,计算文件下载进度
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
                                     didReceiveData:(NSData *)data;

// 请求结束 
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                           didCompleteWithError:(nullable NSError *)error;

4.创建下载任务对象Task

- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request;

代码演示:

#import "ViewController.h"
//写数据到磁盘的沙盒中(Caches文件夹下)  数据下载完之后,数据文件的名字为haha.mp4.名字是自己随便写的
#define fullPath [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"haha.mp4"]

// totalSizefullPath记录的是文件的真实大小。文件大小为9.1M,那么ZB.text文件中就写着9071810。注意ZB.text文件只是用来记录下载的文件的真实大小,ZB.text本身的大小和下载的文件的大小没有任何关系。
#define totalSizefullPath [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"ZB.text"]
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;

/** 文件的总大小*/
@property (nonatomic ,assign) NSInteger totalSize;
/** 当前已经下载数据的总大小*/
@property (nonatomic ,assign) NSInteger currentSize;
/** 文件下载任务*/
@property (nonatomic ,strong) NSURLSessionDataTask *dataTask;
/** 文件句柄*/
@property (nonatomic ,strong) NSFileHandle *handle;
/** 会话对象*/
@property (nonatomic ,strong) NSURLSession *session;
@end

@implementation ViewController

-(NSURLSession *)session
{
    if (_session == nil) {
        //补充知识点:如果delegateQueue参数为nil,那么代理方法是在子线程中调用的,因为代理方法中有刷新进度条的代码(刷新UI),这样刷新进度条的操作就在子线程中调用了(刷新UI,一般要在主线程中调用,不过在子线程中调用也行,只是相对来说,主线程更加适合。一般认为,刷新UI要在主线程调用。这时一种规范。)这里之所以写为[NSOperationQueue mainQueue],是一种规范,因为代理方法中有刷新UI的操作,所以应写主队列(等价于主线程)
         _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    }
    return _session;
}

-(void)viewDidLoad
{
    [super viewDidLoad];
    
    //检查文件是否下载过
   NSDictionary *dictInfo = [[NSFileManager defaultManager] attributesOfItemAtPath:fullPath error:nil];
    
    //获得文件的大小  取出字典dictInfo中的NSFileSize对应的文件大小.因为文件的大小被存储到沙盒中记录(didReceiveResponse:方法中实现了存储下载的文件),所以文件大小并不会因为关闭app而清除。
    //NSInteger size = [dictInfo fileSize];//方法1
    NSInteger size = [dictInfo[@"NSFileSize"] integerValue];;//方法2  等价于方法1
    
    NSLog(@"%@",dictInfo);//第一次运行,打印null
    //不管size有没有值,之前已经下载的文件数据的大小始终为currentSize(size赋值给他了)
    //注意:currentSize就是之前下载过的文件,如果为0,之前下载过的文件大小就为0
    self.currentSize = size;
    
    //读取文件的总大小
    /*强烈注意:
     totalSizefullPath是didReceiveResponse方法中的self.totalSize,  self.totalSize的大小始终是固定的,始终是文件实际的大小,即9.1MB
     详情代码:
     NSData *dataM = [[NSString stringWithFormat:@"%zd",self.totalSize] dataUsingEncoding:NSUTF8StringEncoding];
     [dataM writeToFile:totalSizefullPath atomically:YES];
     */
    
    /*
     初次运行程序:totalSizefullPath为0,因为并没有执行到
     self.totalSize = response.expectedContentLength + self.currentSize;
     只有执行到这句代码,那么totalSizefullPath的值才为文件真实的大小
*/
    NSData *data = [NSData dataWithContentsOfFile:totalSizefullPath];
    if (data) {
        NSInteger totalSize = [[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding] integerValue];
        if (self.currentSize != 0 && totalSize !=0) {
            //计算进度
            //currentSize就是self.currentSize = size;中的。即直线下载过的文件的大小
            //totalSize大小固定为9.1MB,这样当程序运行进入viewDidLoad,如果之前没下载过文件,进度条为0,如果之前下载过文件,那么利用self.currentSize / totalSize得到之前的下载的进度,并且从之前的下载进度开始下载
            CGFloat progress = 1.0 * self.currentSize / totalSize;
            self.progressView.progress = progress;

            NSLog(@"%f",progress);
        }
    }
    
}

//目标:获得之前已经下载的文件数据的大小
//1)之前没有下载过 0
//2)之前下载过
#pragma mark --------------------
#pragma mark lazy loading
-(NSURLSessionDataTask *)dataTask
{
    if (_dataTask == nil) {
     
#warning 完善
        //判断当前文件是否已经下载完毕,如果是那么久不发请求,而是直接提示
        //1)判断进度
        //2)判断currentSize == totalSize && currentSize != 0
        
        //1.确定URL
        NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_01.mp4"];
        
        //2.创建可变的请求对象
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
        
        //2.1 设置请求范围
        /*
         bytes=500-1000 请求从第500到1000个字节的数据
         bytes=-500   请求从第0到500个字节的数据
         bytes= 500-     请求从第500个字节开始的数据,直到整个文件结尾
         */
        NSString *range = [NSString stringWithFormat:@"bytes=%zd-",self.currentSize];
        
        [request setValue:range forHTTPHeaderField:@"Range"];
        NSLog(@"++++++++++++++++%@",range);
        
        //3.设置代理
       
        
        //4.创建Task (调用self.session的懒加载方法)
        _dataTask = [self.session dataTaskWithRequest:request];
    }
    return _dataTask;
}

#pragma mark --------------------
#pragma mark Events
- (IBAction)startBtnClick:(id)sender
{
    [self.dataTask resume];
}

- (IBAction)suspendBtnClick:(id)sender
{
    //暂停任务
     NSLog(@"%s",__func__);
    [self.dataTask suspend];
}

- (IBAction)cancelBtnCLick:(id)sender
{
    NSLog(@"%s",__func__);
    //取消下载操作,该方法是不可以恢复的
    //精华:因为调用了cancel方法,那么self.dataTask已经没有用了(系统底层的cancel方法的.m文件中面已经把self.dataTask变成没有用的了),但是还没有被释放,因为它被strong强指针引用着,我们可以通过手动设置为nil,来释放它。这样当我们点击“继续下载”的时候,goonBtnClick里面的self.dataTask为nil,就会调用懒加载方法,重新创建网络请求,如果仅仅是创建了网络请求,那么一定会重新下载。因为懒加载方法里面设置请求的范围,所以并不会重新开始下载,而是重之前的下载进度上开始下载
    [self.dataTask cancel];
    //不设置为nil,那么继续下载中的self.dataTask懒加载方法不会被调用。因为 懒加载中if (_dataTask == nil)满足时,才执行花括号里面的内容
    self.dataTask = nil;
}
- (IBAction)goonBtnClick:(id)sender
{
    //继续下载|恢复下载
    NSLog(@"%s",__func__);
    [self.dataTask resume];
}

#pragma mark --------------------
#pragma mark NSURLSessionDataDelegate
//1.接收到响应(我们接收到服务器的响应时调用)
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
    //expectedContentLength 获得的是本次网络请求的数据大小 不一定是文件的总大小
    //1.得到文件的总大小
    self.totalSize = response.expectedContentLength + self.currentSize;
   
    /*
     
     要下载的总文件大小为10MB.当点击开始下载,下载了3MB时,点击取消。再点击继续下载时候,调用[self.dataTask resume];中的懒加载方法,执行里面的
     
     NSString *range = [NSString stringWithFormat:@"bytes=%zd-",self.currentSize];//self.currentSize存储着3M
     
     [request setValue:range forHTTPHeaderField:@"Range"];
     
     实现了要从3MB开始下载,下载到10M即 3MB-10MB,通过打印range可知,例如  ++++++++++++++++bytes=785150-
     说明了这个问题
     然后把request加入到session中的代理方法中,执行下载操作
     
     因为之前下载了3MB,所以1.接收到响应的代理方法中的 self.currentSize代表的是3MB, response.expectedContent
    
    */
    
    
    
    //2.把文件总大小的数据写入到磁盘保存起来  self.totalSize整形-->转字符串类型-->转成NSData类型
    NSData *dataM = [[NSString stringWithFormat:@"%zd",self.totalSize] dataUsingEncoding:NSUTF8StringEncoding];
    [dataM writeToFile:totalSizefullPath atomically:YES];
    
    if (self.currentSize == 0) {
        //3.创建一个空的文件
        /*
         第一个参数:文件路径
         第二个参数:文件的数据内容
         第三个参数:文件的属性 nil
         
         - 离线下载:开始下载,边下载边存入沙盒,然后退出程序,此时沙盒中已写入部分数据, 然后进入程序,点击开始下载,此时下载的进度是从之前的进度的基础上继续下载
         - 断点续传:点击取消,然后再点击继续就会从之前的进度开始下载
         
         - 点击取消或者Command+HH,就会把当前没下载完的文件写入到指定的件,可以在指定的文件中查询到下载了多少
         */
        [[NSFileManager defaultManager] createFileAtPath:fullPath contents:nil attributes:nil];
    }
    
    //4.创建文件句柄(文件句柄指向fullPath中的文件).当我们接收到服务器给我们的数据时(不是这个方法,是下面的方法),利用文件句柄向它指向的文件中写数据,然后将数据存到磁盘中
    NSFileHandle *handle = [NSFileHandle fileHandleForWritingAtPath:fullPath];
    
    //移动文件句柄指针指向文件的末尾
    //不移动handle指针,就无法实现离线下载:点击取消或者Comman+HH退出程序时候,handle指针就被释放了,当再进入程序时,这个指针从文件的开头开始写数据,而不是从之前下载的地方开始写数据,只要在边写数据边存入磁盘的同时,边移动handle指针,这样handle指针就能实时的指向存入磁盘中的数据的末尾
   // [handle seekToEndOfFile];//精华 精华 精华 精华 精华  实现了离线下载
    
    self.handle = handle;
    
    NSLog(@"didReceiveResponse---%@",response);//响应头信息
    /* 打印的响应头信息如下
     
     2016-03-24 23:14:12.209 08-掌握-NSURLSession文件下载(dataTask)[12814:925110] didReceiveResponse--- { URL: http://120.25.226.186:32812/resources/videos/minion_01.mp4 } { status code: 206, headers {
     "Accept-Ranges" = bytes;
     "Content-Length" = 9071810;
     "Content-Range" = "bytes 0-9071809/9071810";
     "Content-Type" = "video/mp4";
     Date = "Thu, 24 Mar 2016 15:07:44 GMT";
     Etag = "W/\"9071810-1409456092000\"";
     "Last-Modified" = "Sun, 31 Aug 2014 03:34:52 GMT";
     Server = "Apache-Coyote/1.1";
     } }
     */
    
    
    NSLog(@"%@",fullPath);//文件全路径
    completionHandler(NSURLSessionResponseAllow);
}

//2.接收到数据(我们接收到服务器返回给我们的数据时调用,调用了N次。打印了多少个progress中的内容,这个方法就被调用了多少次)
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    //1.使用文件句柄来写数据到磁盘
    [self.handle writeData:data];
    
    //2.累加当前已经下载的数据的大小
    self.currentSize +=data.length;
    
    //3.计算文件的下载进度
    CGFloat progress = 1.0 * self.currentSize / self.totalSize;
    
    self.progressView.progress = progress;
    NSLog(@"%f-%@",progress,[NSThread currentThread]);
}

//3.请求结束(成功|失败)  (我们请求向服务器请求完成后调用,无论请求成功还是失败)
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    //1.关闭文件句柄
    [self.handle closeFile];
    self.handle = nil;
    
    NSLog(@"%@",fullPath);
}
@end

截图


31-12.gif

方式四:dataTask+delegate+NSOutputStream(输出流)
  • 和文件句柄的不同之处:将创建文件句柄用到的代码以及和文件句柄相关的代码替换为输出流的代码,其余的不用变化
    • 具体不同:
      • @property (nonatomic ,strong) NSOutputStream *stream;不同
      • 3个代理方法中的部分内容不同

代码演示:

#import "ViewController.h"
// 写数据到磁盘的沙盒中(Caches文件夹下)  数据下载完之后,数据文件的名字为minion_01.mp4.名字自己定
#define fullPath [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"minion_01.mp4"]
// totalSizefullPath记录的是文件的真实大小。文件大小为9.1M,那么ZB.text文件中就写着9071810。注意ZB.text文件只是用来记录下载的文件的真实大小,ZB.text本身的大小和下载的文件的大小没有任何关系。
#define totalSizefullPath [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"ZB.text"]
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;

/** 文件的总大小*/
@property (nonatomic ,assign) NSInteger totalSize;
/** 当前已经下载数据的总大小*/
@property (nonatomic ,assign) NSInteger currentSize;
/** 文件下载任务*/
@property (nonatomic ,strong) NSURLSessionDataTask *dataTask;
/** 文件句柄*/
//@property (nonatomic ,strong) NSFileHandle *handle;
/** 输出流*/
@property (nonatomic ,strong) NSOutputStream *stream;
/** 会话对象*/
@property (nonatomic ,strong) NSURLSession *session;
@end

@implementation ViewController

//目标:获得之前已经下载的文件数据的大小
-(void)viewDidLoad
{
    [super viewDidLoad];
    
    //检查文件是否下载过(参数fullPath是沙盒中的Caches目录,所以NSFileManager检查的是Caches目录中的信息)
    NSDictionary *dictInfo = [[NSFileManager defaultManager] attributesOfItemAtPath:fullPath error:nil];
    
    //获得文件的大小,用size表示。
    //NSInteger size = [dictInfo fileSize];;
    NSInteger size = [dictInfo[@"NSFileSize"] integerValue];;
    
    NSLog(@"%@",dictInfo);
    self.currentSize = size;
    
    //读取文件的总大小
    NSData *data = [NSData dataWithContentsOfFile:totalSizefullPath];
    if (data) {
        NSInteger totalSize = [[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding] integerValue];
        if (self.currentSize != 0 && totalSize !=0) {
            //计算进度
            CGFloat progress = 1.0 * self.currentSize / totalSize;
            
            self.progressView.progress = progress;
            
            NSLog(@"%f",progress);
        }
    }
    
}

-(void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];
    [self.session invalidateAndCancel];
}

#pragma mark --------------------
#pragma mark lazy loading

-(NSURLSession *)session
{
    if (_session == nil) {
        //补充知识点:nil 那么代理方法是在子线程中调用的
        _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    }
    return _session;
}

-(NSURLSessionDataTask *)dataTask
{
    if (_dataTask == nil) {
     
#warning 完善
        //判断当前文件是否已经下载完毕,如果是那么久不发请求,而是直接提示
        //1)判断进度
        //2)判断currentSize == totalSize && currentSize != 0
        
        //1.确定URL
        NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_01.mp4"];
        
        //2.创建可变的请求对象
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
        
        //2.1 设置请求范围
        /*
         bytes=500-1000 请求从第500到1000个字节的数据
         bytes=-500   请求从第0到500个字节的数据
         bytes= 500-     请求从第500个字节开始的数据,直到整个文件结尾
         */
        NSString *range = [NSString stringWithFormat:@"bytes=%zd-",self.currentSize];
        
        [request setValue:range forHTTPHeaderField:@"Range"];
        NSLog(@"++++++++++++++++%@",range);
        
        //3.设置代理(懒加载)
       
        
        //4.创建Task
        _dataTask = [self.session dataTaskWithRequest:request];
    }
    return _dataTask;
}

#pragma mark --------------------
#pragma mark Events
- (IBAction)startBtnClick:(id)sender
{
    [self.dataTask resume];
}

- (IBAction)suspendBtnClick:(id)sender
{
    //暂停任务
     NSLog(@"%s",__func__);
    [self.dataTask suspend];
}

- (IBAction)cancelBtnCLick:(id)sender
{
    NSLog(@"%s",__func__);
    //取消下载操作,该方法是不可以恢复的
    [self.dataTask cancel];
    self.dataTask = nil;
}
- (IBAction)goonBtnClick:(id)sender
{
    //继续下载|恢复下载
    NSLog(@"%s",__func__);
    [self.dataTask resume];
}

#pragma mark --------------------
#pragma mark NSURLSessionDataDelegate
//1.接收到响应
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
    //expectedContentLength 获得的是本次网络请求的数据大小(总大小-之前已下载的大小),不一定是文件的总大小
    //1.得到文件的总大小
    self.totalSize = response.expectedContentLength + self.currentSize;
    
    //2.把文件总大小的数据写入到磁盘保存起来
    NSData *dataM = [[NSString stringWithFormat:@"%zd",self.totalSize] dataUsingEncoding:NSUTF8StringEncoding];
    [dataM writeToFile:totalSizefullPath atomically:YES];
    

    //3.创建输出流
    /*
     第一个参数:路径 文件数据保存的位置
     第二个参数:是否是追加
     如果输出流发现指向路径下面文件不存在那么会自动的创建一个空的文件
     */
    NSOutputStream *stream = [[NSOutputStream alloc]initToFileAtPath:fullPath append:YES];
    
    //4.打开输出流
    [stream open];
    
    self.stream = stream;
    
  
    NSLog(@"didReceiveResponse---%@",response);
    
    NSLog(@"%@",fullPath);
    completionHandler(NSURLSessionResponseAllow);
}

//2.接收到数据
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
    //1.使用输出流写数据到磁盘
    [self.stream write:data.bytes maxLength:data.length];
    
    //2.累加当前已经下载的数据的大小
    self.currentSize +=data.length;
    
    //3.计算文件的下载进度
    CGFloat progress = 1.0 * self.currentSize / self.totalSize;
    
    self.progressView.progress = progress;
    NSLog(@"%f-%@",progress,[NSThread currentThread]);
}

//3.请求结束(成功|失败)或者调用cancel方法
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    //关闭输出流
    [self.stream close];
    self.stream = nil;
    // 下载完数据或者执行cancel才会打印
    NSLog(@"%@",fullPath);
}
@end
  

截图:

31-13.gif

注意点:

  • NSURLSessionDataTask和AFN参数的区别

  • NSURLSessionDataTask中,data是响应体信息,response是响应头信息;

  • AFN中responseObject是响应体信息,不是响应头信息。内部已经完成了序列化处理,通过task.response获取到响应头信息

  • MJExtension框架不用一个一个地把数组里面的数据转成模型,只需在模型方法里面声明属性,然后在外界通过这种形式调用即可self.videos = [ZBVideo mj_objectArrayWithKeyValuesArray:dictM[@"videos"]];

  • 有了这句代码,模型中声明的ID属性本质对应着的是数据库中的id

     //0.告诉框架新值和旧值的对应关系
     [ZBVideo mj_setupReplacedKeyFromPropertyName:^NSDictionary *{
         return @{
                  @"ID":@"id",
                  };
         
     }];
     
    
  • NSURLSession可以实现后台下载和上传文件

  • 解决下载进度始终为100%的办法:将运行的app卸载掉即可。

  • 离线下载条件:

    • 1:知道从哪个地方开始下载,即获取已经下载的文件数据,
    • 2:实现请求某一部分数据(通过设置请求范围)
  • 对于1,有两种解决办法:

    • a:退出程序时,将currentSize的值存入沙盒,再次运行程序,把这个值读取出来(麻烦,用b做法)
    • b:用下面的代码
  //检查文件是否下载过
   NSDictionary *dictInfo = [[NSFileManager defaultManager] attributesOfItemAtPath:fullPath error:nil];
    
    //获得文件的大小   取出字典dictInfo中的NSFileSize
    //NSInteger size = [dictInfo fileSize];//方法1
    NSInteger size = [dictInfo[@"NSFileSize"] integerValue];//方法2  等价于方法1
    
    NSLog(@"rrrrrrrrrrrrrr%@",dictInfo);
    self.currentSize = size;//不管size有没有值,之前已经下载的文件数据的大小始终为currentSize
    

  • 对于2:有1种解决办法

        NSString *range = [NSString stringWithFormat:@"bytes=%zd-",self.currentSize];
        
        [request setValue:range forHTTPHeaderField:@"Range"];

文件下载总结:

  • 使用downloadTask下载文件(两种方式):

    • 使用block的方式,适用于小文件,无法监听文件的下载进入,自动写磁盘
    • 使用delegate的方式,可以实现自动写磁盘+监听进度+断点续传;缺点:无法实现离线断点下载(就是重新启动程序,能够继续之前的下载进度)
  • 使用dataTask下载文件:

    • 利用delegate,监听进度,缺点:内存飙升
  • 使用文件句柄下载文件:

    • 解决内存飙升的问题:利用NSFileHandle-->边接收数据,边存到沙盒
    • 文件总大小写入到磁盘,这样即使程序退出,写入到磁盘的内容也不会消失,当我们再次启动程序时,可以利用代码获取到之前写入到磁盘的数据的大小
    • 文件句柄和输出流(相当于水管)一样。但是输出流省去了创建文件的步骤,代码简单。实现取消之后,点击继续下载,还能接着之前的进度下载前提:知道下载到哪一个部分了+实现能下载任意一部分的数据的功能(设置请求头Range的范围)
// 普通的方式:拼接数据(一次一次第调用所在的方法,从而一次次的将传递过来的data拼接到fileData中)
[self.fileData appendData:data];

// 优化的方式.使用文件句柄来写数据到磁盘(每次调用所在的方法,就把data写入到handle中),就因为这一点的不同,就可以解决内存飙升的问题
[self.handle writeData:data];

内存飙升问题

  • 利用downloadTask代理下载文件出现内存飙升的问题

NSURLSession实现文件下载_第3张图片
31-03.png

  • 利用downloadTask代码块下载文件出现内存飙升的问题
NSURLSession实现文件下载_第4张图片
31-04.png

  • 利用文件句柄下载文件可以解决内存飙升的问题
NSURLSession实现文件下载_第5张图片
31-02.png

注意fileURLWithPath和URLWithString的参数的区别

  • 执行剪切操作
//执行剪切操作  fullPath是个路径
    [[NSFileManager  defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:fullPath] error:nil];
  • 确定URL
//确定URL
    NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_01.mp4"];

你可能感兴趣的:(NSURLSession实现文件下载)