NSURLSession文件下载之文件句柄和输出流

一 NSURLSession文件下载

方法一:采用block块回调的方式进行文件下载
涉及到的知识点:
1> 如何将下载的文件存入沙盒中?(直接看代码)
2> 如何拼接文件保存路径?(直接看代码)
block回调优缺点和应用范围:
//特点:能够直接把文件下载到沙盒中,我们需要做文件剪切处理(不会有内存飙升的问题)
//缺点:我们无法监听文件的下载进度
//应用:适用于小文件下载
3 代码中包括文件下载好存入的沙盒路径和文件的拼接
#pragma mark - 下载视频(block回调)
- (void)downloadWithVideo
{
    //确定请求路径
    NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_02.mp4"];

    //创建可变的请求对象
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];

    //创建会话
    NSURLSession *session = [NSURLSession sharedSession];

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

        NSLog(@"%s------",__func__);

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

        //剪切文件到到制定的位置
        [[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:fullName] error:nil];

        //打印下载的文件路径
        NSLog(@"------------%@",fullName);

    }];

    //执行task任务
    [downloadTask resume];
}
4 获取文件路径和剪切下载好的文件到指定的目录中的原因:由于下载好的文件默认是存在temp中的,但是如果没指定好将下载好的文件存入你所指定的位置,那么是找不到下载好的文件.原因是,temp文件是用来保存零时数据的,所以我们需要将下载好的文件存入caches中,因为文件都是存在caches文件中的.
方法二:采用代理的方法进行文件下载
涉及到知识点:
1> 通过手动控制文件的状态(开始下载;继续下载;暂停下载;取消下载)
2> 在storyboard中显示下载的进度
3> 经常需要调用的四个代理方法
4> 如何计算进度
运用代理方法进行下载文件的优缺点:
优点:1> 能够直接把文件下载到沙盒中,我们需要做文件剪切处理(不会有内存飙升的问题)
    2> 可以监测文件的下载进度
缺点:NSURLSessionDownloadTask不能实现断点下载(完全退出程序后不能从上一次下载的地方继续下载)
应用:大小文件都可以下载
相关代码及解释
1 用户需要控制的文件下载状态
#pragma mark - 开始下载

- (IBAction)startBtnClick
{
    [self downloadWithDataDelegate];
}
#pragma mark - 继续下载

- (IBAction)goOnBtnClick
{
    //判断有没有数据
    if (self.resumeData) {
        //在原来的基础上继续下载
        self.downloadTask = [self.session downloadTaskWithResumeData:self.resumeData];
    }

    //继续下载
    [self.downloadTask resume];
}
#pragma mark - 暂停下载

- (IBAction)suspendBtnClick
{
    [self.downloadTask suspend];
}
#pragma mark - 取消

- (IBAction)cancelBtnClick
{
    //该方法取消是可以恢复的
    [self.downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
       //已经下载好的数据赋值
        self.resumeData = resumeData;

    }];
}
2 主方法(需用self调用该方法,才能执行下载操作)
#pragma mark - 下载视频的代理方法
- (void)downloadWithDataDelegate
{

    //确定路径
    NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_02.mp4"];

    //创建请求对象
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];

    //创建task任务(用最简单的方法创建)
    NSURLSessionDownloadTask *downloadTask = [self.session downloadTaskWithRequest:request];

    //执行task任务
    [downloadTask resume];

    self.downloadTask = downloadTask;
}
3 懒加载会话(由于我们整个类很多地方都需要使用到会话,那么我们创建一个懒加载,不需要考虑什么时候创建,需要用到的时候就创建)
#pragma mark - 懒加载会话

- (NSURLSession *)session
{
    if (_session == nil) {
        _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    }
    return _session;
}
4 代理方法一:保存下载好的文件
#pragma mark - 下载完成(在这个方法里面完成对数据的存储)
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location
{
    //location     文件的临时存储路径(磁盘/tmp)
    //获取文件的全路径
    NSString *fullPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:downloadTask.response.suggestedFilename];

    //剪切文件到到制定的位置
    [[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:fullPath] error:nil];
}
5 下载数据,并且给进度条赋值
#pragma mark - 下载数据
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
    NSLog(@"%s",__func__);

    //计算精度
    CGFloat progress = 1.0 * totalBytesWritten / totalBytesExpectedToWrite;

    //给进度条赋值
    self.progressView.progress = progress;
}
6 如何计算下载的精度?
—解答: [ 1.0 *(用下载好的数 / 总的文件大小数)]–>乘以1.0是将整型转为浮点型

二 文件的断点续传(文件句柄)

1 概念:断点续传就是当用户在下载文件的时候,但是用户突然不想下载了,然后又没有点击取消,而是直接将app退出.但是等到用户再次运行app的时候又突然想下载该文件了,而这里就需要用到断点续传的方法,就是让用户接着从上一次下载的地方接着下载,不需要开始在下载.
—方法: 我所提到的上面的方法都不能满足断点续传的目的,这里我将采用NSURLSessionDataTask这个类的方法实现实现目的.
注意: 使用NSURLSessionDataTask这个类进行文件下载中会涉及到的难点是:
​1> 内存飙升的原因是文件拼接
—–​解决办法: 使用文件句柄作为拼接数据的方法
2> 进度不准确
3> 文件不完整
4>文件句柄二次创建产生的文件大小太大的问题
5> 解决当用户再次下载的时候,进度条显示到上一次下载的地点
—–​解决办法:做判断
2 文件句柄的基本概念
文件句柄:相当于一个指针,指向文件的末尾,功能是能达到文件的追加,不会使得断点下载的文件重叠.
3 代码部分
3.1 相关宏
/沙盒minion_02.mp4的文件路径
#define fullPath [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"minion_02.mp4"]
//用XFJ.XFJ将数据占时保存起来
#define totalPath [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"XFJ.XFJ"]
3.2 属性和协议
@interface ViewController ()<NSURLSessionDataDelegate>
//storyboard中的进度条
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;

//创建task任务属性
@property (nonatomic, strong) NSURLSessionDataTask *dataTask;

//总文件大小
@property (nonatomic, assign) NSInteger totalSize;

//设置文件句柄的属性
@property (nonatomic, strong) NSFileHandle *handle;

//当前的下载数据
@property (nonatomic, assign) NSInteger currentSize;
//会话
@property (nonatomic, strong) NSURLSession *session;
3.3 用户的具体操作
#pragma mark - 开始下载
- (IBAction)startBtnClick
{
    [self.dataTask resume];
}
#pragma mark - 暂停下载
- (IBAction)suspendBtnClick
{
    [self.dataTask suspend];
}
#pragma mark - 取消下载
- (IBAction)cancelBtnClick
{
    [self.dataTask cancel];

    //取消下载就讲任务清空
    self.dataTask = nil;
}
#pragma mark - 继续下载
- (IBAction)goOnBtnClick
{
    [self.dataTask resume];
}
3.4 对会话对象的懒加载和代理的设置
#pragma mark - 懒加载会话,如果不懒加载的话,那么每次运行的时候都会创建一个行的会话对象
- (NSURLSession *)session
{
    if (_session == nil) {
        //如果没有会话对象的话就创建一个(里面的队列参数可以填nil,填nil默认在子线程中进行下载)
        _session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
    }
    return _session;
}
3.5 用户二次运行app的时候对进度条的设置
#pragma mark - view加载完毕的时候调用

- (void)viewDidLoad
{
    [super viewDidLoad];

    //判断文件是否被下载过
    //拿到文件的保存目录
    NSDictionary *dictInfo = [[NSFileManager defaultManager] attributesOfItemAtPath:fullPath error:nil];

    //获取文件
    NSUInteger size = [dictInfo[@"NSFileSize"] integerValue];
    //拿到里面已经下载的文件数据赋值作为当前要开始往后下载的起点

    self.currentSize = size;

    //读取临时文件的总大小
    NSData *data = [NSData dataWithContentsOfFile:totalPath];

    //判断data是否有值,如果有值的话,那么就将二进制值转为NSInteger类型
    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;
        }
    }
}
3.6 懒加载对任务的创建,由于是需要用到文件句柄,也就需要将文件的数据分割为各小段.
#pragma mark - 任务的懒加载
- (NSURLSessionDataTask *)dataTask
{
    if (_dataTask == nil) {
        //确定文件路径
        NSURL *url = [NSURL URLWithString:@"http://120.25.226.186:32812/resources/videos/minion_02.mp4"];

        //创建请求对象
        NSMutableURLRequest *requeset = [NSMutableURLRequest requestWithURL:url];

        //设置请求范围
        NSString *range = [NSString stringWithFormat:@"bytes=%zd-",self.currentSize];

        [requeset setValue:range forHTTPHeaderField:@"Range"];

        //创建task任务
        _dataTask = [self.session dataTaskWithRequest:requeset];
    }
    return _dataTask;
}
3.7 数据源方法
#pragma mark - 数据源代理
#pragma mark - 接收响应
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
    //得到文件的总大小expectedContentLength(获得是本次网络请求的数据大小)
    //当退出整个程序的时候,再次运行程序的时候,数据总数需要加上当前保存的数据然后加上本次网络请求的数据.
    self.totalSize = response.expectedContentLength + self.currentSize;

    //把文件的总大小写入磁盘,以便于当用户二次下载的时候,直接读取出磁盘的数据,设置到进度条上,给用户好的体验
    NSData *dataM = [[NSString stringWithFormat:@"%zd",self.totalSize] dataUsingEncoding:NSUTF8StringEncoding];
    //将取出来的数据保存到临时的文件中
    [dataM writeToFile:totalPath atomically:YES];
    //这里判断一下,是否当前的属性有数据,如果没有的话,在创建一个空的文件夹用来下载数据,如果有的话,那么就不需要再创建一个空的文件夹,直接用之前的就可以
    if (self.currentSize == 0) {
        //创建一个空的文件
        /*
         第一个参数:文件路径
         第二个参数:文件的数据内容
         第三个参数:文件的属性 nil
         */
        [[NSFileManager defaultManager] createFileAtPath:fullPath contents:nil attributes:nil];
    }
    //创建文件句柄
    NSFileHandle *handle = [NSFileHandle fileHandleForWritingAtPath:fullPath];


    //移动文件句柄到末尾
//    [handle seekToEndOfFile ];
    //赋值
    self.handle = handle;

    //通过completionHandler回调.告诉系统处理数据
    //该句必须写,否则是无法得到数据的
    completionHandler(NSURLSessionResponseAllow);  
}
3.8 移动文件句柄,计算进度条显示
#pragma mark - 接收数据
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
{
     //移动文件句柄到末尾
    [self.handle seekToEndOfFile ];

    //使用文件句柄来写数据到磁盘中
    [self.handle writeData:data];

    //累加当前已经下载的数据大小
    self.currentSize += data.length;

    //结算下载精度
    CGFloat progress = 1.0 * self.currentSize / self.totalSize;

    //赋值
    self.progressView.progress = progress;
}
​3.9 关闭文件句柄,释放文件句柄
#pragma mark - 请求结束时候调用
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    //关闭文件句柄
    [self.handle closeFile];

    //将文件句柄赋值为空
    self.handle = nil;
}

三 文件下载(输出流)

1 输出流:说明白点就是类似于管道通水一样下载,只不过输出流比起文件句柄多了自身会创建一个空的文件夹,也就是如果发现所指向的路径不存在,那么会自动创建一个空的文件夹,用来装数据.
2 代码
​2.1 属性
//设置输出流属性@property (nonatomic, strong) NSOutputStream *stream;
2.2 将接受响应中的文件句柄的相关代码换成下面的代码
//创建输出流
    NSOutputStream *stream = [[NSOutputStream alloc] initToFileAtPath:fullPath append:YES];

    //开启输出流
    [stream open];

    //赋值输出流属性
    self.stream = stream;
2.3 请求结束的时候关闭输出流,并且释放输出流,否则会造成内存泄露
//关闭输出流
    [self.stream close];

    //将文件句柄赋值为空
    self.stream = nil;

四 NSURLSession的强引用

说明:在整个程序中,我们都对会话进行了强引用,这样会造成内存泄露
解决办法:
#pragma mark - 释放会话(在view显示完毕后)
- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];

    [self.session invalidateAndCancel];
}

- (void)dealloc
{
    NSLog(@"%s",__func__);
}

五 总结

1 认识文件句柄和输出流,知道怎么用.数据是如何保存的?怎么通过再次的赋值达到断点续传?这些都是需要知道的,我里面的代码都说明白了,一些逻辑问题,如果不懂的话就看代码,里面都有详细解释,要是真有什么难点的话,就给我留言.谢谢!!!!
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值