zoukankan      html  css  js  c++  java
  • 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 : 普通的GETPOST请求
        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];
        }
        
    }
  • 相关阅读:
    潘石屹出售上海外滩金融中心股权 4年没新增投资
    【BZOJ4036】【洛谷3175】【HAOI2015】—按位或(FMT+期望dp)
    信托配资清理“逃生通道”隐情
    中国在移动端的营销水平已超国外
    “土豪”们的新人生模型
    苏宁的逆市“O2O进化论”
    信汇中正领导力打造“必读12篇”之路
    你是否真的需要说声“谢谢”?
    新主管如何快速上手
    雷军和黄章又掐架了 不就是“不服跑个分”嘛
  • 原文地址:https://www.cnblogs.com/gaozhang12345/p/10818704.html
Copyright © 2011-2022 走看看