• (一二六)单线程文件下载与断点续传


    本文讨论单线程的文件下载和断点续传,通过从本地服务器下载一个较大的文件,实现显示进度、中途暂停与断点续传。

    下载过程大致如下:

    ①通过NSURL创建指向特定下载地址的请求,本文中下载的文件位于网站根目录的lesson1下的nav.dmg,因此URL应为http://127.0.0.1/lesson1/nav.dmg。

    ②通过NSURL创建URLRequest,为了能够更改HTTP请求头,实现特定字节的下载,使用NSMutableURLRequest,然后设置请求头的Range范围,range的主要写法是bytes=x-y,代表下载x-y的字节,注意字节的起始是0。也可以忽略一端,例如x-代表下载x和以后的字节;而-y表示下载最后y个字节。

    ③使用NSURLConnection执行请求,并且设置代理,通过代理方法接收数据。

    为了能够实时计算进度,我们使用一系列变量,记录下载了的字节数和总字节数:

    @property (nonatomic, assign) long long totalLength;
    @property (nonatomic, assign) long long currentLength;

    【NSURLConnection的代理方法】

    - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error

    当出错时来到这里。

    - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response

    这个比较重要,在下载文件之前会先拿到这个响应头,从中可以拿到文件的大小,在这里适合做一些初始化工作,例如0B文件的创建和总大小、当前大小的初始化。

    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data

    每当文件下载完一小块,就会调用一次,从这里可以进行文件拼接,进度更新。

    - (void)connectionDidFinishLoading:(NSURLConnection *)connection

    当文件下载完成时调用这个方法,一般在这里关闭文件。

    很显然网络畅通时的调用顺序是②->多次③->④。

    【文件的操作】

    ①通过NSFileManager创建空文件,用于数据拼接。

    // 先创建一个空文件
    NSFileManager *mgr = [NSFileManager defaultManager];
    NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:@"download.dmg"];
    [mgr createFileAtPath:path contents:nil attributes:nil];
    ②为了能够方便拼接,使用NSFileHandle指向文件,并且保存成成员变量。

    @property (nonatomic, strong) NSFileHandle *handle;
    // 使用文件句柄操作文件
    NSFileHandle *handle = [NSFileHandle fileHandleForWritingAtPath:path];
    _handle = handle;
    _totalLength = response.expectedContentLength;
    _currentLength = 0;
    
    ③文件的拼接

    通过NSFileHandle的seekToEndOfFile方法可以指向当前文件尾,然后使用writeData方法向后拼接。

    // 下载拼接思路:先创建空文件,然后一部分一部分的拼接
    [self.handle seekToEndOfFile];
    [self.handle writeData:data];
    总体来说,思路是一部分一部分的拼出最终文件。

    【断点续传】

    暂停只能通过结束NSURLConnection实现,connection一旦结束就会失效,只能重新创建。

    [self.connection cancel];
    self.connection = nil;
    为了实现下次再从中断的位置下载,只需要通过_currentLength即可,让Range为bytes:_currentLength-即可,因为字节是从0开始的,当前长度为len代表下一个要下载的字节编号就是len。

    请求代码如下:

    NSURL *url = [NSURL URLWithString:@"http://127.0.0.1/lesson1/nav.dmg"];
    // 通过设置请求头range来设置下载数据的范围
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    NSString *range = [NSString stringWithFormat:@"bytes=%lld-",self.currentLength];
    [request setValue:range forHTTPHeaderField:@"Range"];
    self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
    另一个注意点是因为新创建了请求,会再去获取响应头,这样又会用空文件覆盖原文件,重新下载,我们加一个判断,如果_currentLength≠0,直接返回即可。


    下面是完整的代码,其中downloadBar是进度条,btn用于控制下载开始与暂停,二者均来自storyboard。btn点击后延时0.5秒开始下载是为了防止点击后按钮被阻塞无法到达暂停键状态。

    //
    //  ViewController.m
    //  大文件下载
    //
    //  Copyright (c) 2015 soulghost. All rights reserved.
    //
    
    #import "ViewController.h"
    
    @interface ViewController () <NSURLConnectionDataDelegate>
    
    @property (nonatomic, strong) NSFileHandle *handle;
    @property (nonatomic, assign) long long totalLength;
    @property (nonatomic, assign) long long currentLength;
    @property (weak, nonatomic) IBOutlet UIProgressView *downloadBar;
    @property (weak, nonatomic) IBOutlet UIButton *btn;
    
    @property (nonatomic, strong) NSURLConnection *connection;
    
    @end
    
    @implementation ViewController
    
    - (void)startDownload{
        
        NSURL *url = [NSURL URLWithString:@"http://127.0.0.1/lesson1/nav.dmg"];
    
        // 通过设置请求头range来设置下载数据的范围
    
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
        NSString *range = [NSString stringWithFormat:@"bytes=%lld-",self.currentLength]; // 从0开始,下到10应该是0-9.下面应该下载10.
        [request setValue:range forHTTPHeaderField:@"Range"];
        self.connection = [NSURLConnection connectionWithRequest:request delegate:self];
        
    }
    
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
        
        
        
    }
    
    - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
        
        NSLog(@"下载出错");
        
    }
    
    - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{
        
        if (_currentLength) {
            return;
        }
        
        // 先创建一个空文件
        NSFileManager *mgr = [NSFileManager defaultManager];
    
        NSString *path = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject] stringByAppendingPathComponent:@"download.dmg"];
        [mgr createFileAtPath:path contents:nil attributes:nil];
        
        // 使用文件句柄操作文件
        NSFileHandle *handle = [NSFileHandle fileHandleForWritingAtPath:path];
        _handle = handle;
        _totalLength = response.expectedContentLength;
        _currentLength = 0;
        self.downloadBar.progress = 0;
        
        
    }
    
    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
        
        // 下载拼接思路:先创建空文件,然后一部分一部分的拼接
        [self.handle seekToEndOfFile];
        [self.handle writeData:data];
        
        self.currentLength += data.length;
        self.downloadBar.progress = (float)self.currentLength / self.totalLength;
        NSLog(@"进度:%f",self.downloadBar.progress);;
        
    }
    
    - (void)connectionDidFinishLoading:(NSURLConnection *)connection{
        
        [self.handle closeFile];
        [self.btn setTitle:@"开始" forState:UIControlStateNormal];
        self.currentLength = 0;
        
    }
    
    - (IBAction)btnClick:(UIButton *)sender {
        
        if ([sender.titleLabel.text isEqualToString:@"开始"]) {
            [sender setTitle:@"暂停" forState:UIControlStateNormal];
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                [self startDownload];
            });
        }else{
            [sender setTitle:@"开始" forState:UIControlStateNormal];
            [self.connection cancel];
            self.connection = nil;
        }
        
        
    }
    
    @end
    

  • 相关阅读:
    silo 主机 报找不到 grain 实现错误的一个注意
    转:CRT注册
    Maven生命周期
    Maven学习笔记
    Java内存回收机制
    Selenium2.0和1.0的区别
    关于使用Selenium RC无法打开指定页面问题
    四儿子购买手册
    Objective-C 宏定义的收集
    设计模式:适配器模式
  • 原文地址:https://www.cnblogs.com/aiwz/p/6154029.html
Copyright © 2020-2023  润新知