浅谈iOS多任务断点下载

作者: 旅行的光 | 来源:发表于2017-01-19 18:42 被阅读2169次

    前言

    在iOS开发当中,文件的下载是经常需要用到的一个功能,尤其是大文件的断点下载。众所周知,苹果为开发者提供了两个比较好用的原生处理网络请求的类:
    1,NSURLConnection
    2,NSURLSession
    当然在GitHub这个世界上最大的同性交友网站😳,你还可以找到很多处理网络请求的第三方库,例如AFNetWorking等 ...

    这篇文章将主要介绍如何使用NSURLConnection和NSURLSession去实现断点下载,并且我自己使用NSURLConnection封装了一个处理多个下载任务的断点下载库,这个库现在主要支持以下功能:
    1,支持多任务下载管理
    2,支持断点下载
    3,支持后台下载

    文中所使用的所有Demo和已经封装好的库都在GitHub: JXDataTransimission

    Talk is cheap, show me your demo!

    点我下载.gif

    Http浅谈

    上面的Demo可能会让大家感到疑惑,为什么后两个下载下来的文件会比文件实际大小要大呢?是不是代码写的有问题?
    如果你对于Http有一定的了解那么你会知道,我们在向服务器发送Http请求之后,服务器会给我们一个响应,在响应头中包含一些服务器信息,其中有两个内容是我们这次需要特别注意的:
    1,Accept-Range:说明服务器是否支持range设置
    2,Content-Length:说明我们这次所请求的内容总长度

    那么让我们来看一下以上其中两个服务器响应头的内容吧:

    支持Range.png 不支持Range.png

    从上面的图片我们可以清楚的看到响应头的内容,其中一个是不支持Range的。

    其实在写这个demo之前我看了有些文章谈到了range的用法,当我发现其中有下载链接不能实现正确的断点续传之后我一度怀疑是自己的代码有问题,之后通过Google发现其实只是服务器不支持range而已。😭这也许就是一个菜鸟的辛酸😔吧,很多时候看到别人写的东西只知其然,不知其所以然。

    NSURLConnection的使用

    很多朋友会说,苹果已经停止了对NSURLConnection的支持,我们没有必要再了解这个类了,这个说法没有错,但是我认为如果想对于下载过程有一个更好的认识,我们最好还是自己去写一个NSURLConnection的下载,因为相对于NSURLSession的强大封装,NSURLConnection需要我们自己实现下载的细节内容,因此也有助于我们理解断点续传的逻辑。

    An NSURLConnection object lets you load the contents of a URL by providing a URL request object. The interface for NSURLConnection is sparse, providing only the controls to start and cancel asynchronous loads of a URL request. You perform most of your configuration on the URL request object itself.

    URLConnection通过URL请求对象下载内容,它可以提供URL请求的异步下载,我们可以自己配置URL 请求对象,以此来实现下载。

    我们使用的NSURLConnection主要方法和代理

    在这个Demo中我们使用了以下方法:

    数据异步下载.png 取消下载.png

    以上的方法注释已经很清楚了,在这里我想说的是一个问题:
    若果我们使用-sendAsynchronousRequest:queue:completionHandler:方法创建NSURLConnection对象的话,那么只有当下载完成的时候才能获得block回掉,它返回的信息十分有限,我们不能通过这个方法查看下载过程中的情况。
    因此,我们需要使用设置代理的其它几个方法去创建NSURLConnection对象。

    接下来让我们看一下代理吧。
    主要使用了两个代理:
    1,NSURLConnectionDelegate: 主要处理链接时的一些设置和请求。
    2, NSURLConnectionDataDelegate:主要处理数据传输中的重要信息。

    在我上传的Demo中有一个视图控制器叫做ConnectionViewController,在其中实现了NSURLConnection的断点续传下载。

    NSURLConnection断点续传.gif

    Show me the code!

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        NSString *urlStr = @"http://120.25.226.186:32812/resources/videos/minion_02.mp4";
        NSURL *url = [NSURL URLWithString:urlStr];
        self.urlRequest = [NSMutableURLRequest requestWithURL:url];
        
        self.progressLabel.textAlignment = NSTextAlignmentCenter;
    }
    
    - (void)didReceiveMemoryWarning {
        [super didReceiveMemoryWarning];
        // Dispose of any resources that can be recreated.
    }
    - (IBAction)btnClick:(UIButton *)sender {
        switch (sender.tag) {
            case 100:
                [self startDownloadFile];
                break;
            case 101:
                [self stopDownloadFile];
                break;
            case 102:
                [self cancelDownloadFile];
                break;
            default:
                break;
        }
    }
    
    
    - (void)startDownloadFile {
        self.connection = [NSURLConnection connectionWithRequest:_urlRequest delegate:self];
        
    }
    
    - (void)stopDownloadFile {
        NSString *range = [NSString stringWithFormat:@"bytes:%lld-",_currentLength];
        [_urlRequest setValue:range forHTTPHeaderField:@"Range"];
        NSLog(@"URL Request:%@",_urlRequest);
        NSLog(@"Range:%@",range);
        [_connection cancel];
        _connection = nil;
    }
    
    - (void)cancelDownloadFile {
        
    }
    
    #pragma mark -- NSURLConnectionDataDelegate
    //当链接建立之后,服务器会向客户端发送响应,此时这个代理方法会被自动调用,只调用一次。
    - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
        if (!_totalLength) {
            self.totalLength = response.expectedContentLength;
            NSLog(@"Total Length:%lld",_totalLength);
        }
        if (!_fileManager) {
            _fileManager = [NSFileManager defaultManager];
            NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
            NSString *filePath = [caches stringByAppendingPathComponent:response.suggestedFilename];
            self.filePath = filePath;
            [_fileManager createFileAtPath:filePath contents:nil attributes:nil];
            NSLog(@"File Path:%@",self.filePath);
        }
    
    }
    //当客户端收到数据之后,会调用这个方法,这个方法将被多次调用
    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
       
        if (!_fileHandle) {
            self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:_filePath];
        }
        
        [self.fileHandle seekToEndOfFile];
        [self.fileHandle writeData:data];
        self.currentLength += data.length;
        self.progressView.progress = (double)_currentLength / _totalLength;
        self.progressLabel.text = [NSString stringWithFormat:@"%.2f / 100",(double)_currentLength/_totalLength * 100];
    }
    //当下载结束之后调用这个方法
    - (void)connectionDidFinishLoading:(NSURLConnection *)connection {
        [self.connection cancel];
        self.connection = nil;
        [_fileHandle closeFile];
        self.fileHandle = nil;
        NSLog(@"Length After Loading:%lld",_currentLength);
    }
    

    代码比较容易理解,在这里只说两个需要注意的地方
    1,使用fileHandle来确定每次数据需要写入本地文件的位置,通过这个办法将断点续传之后下载的数据写入本地文件的正确位置。
    2,通过设置NSURLMutableRequest的请求头来改变range的值,这样每次断点之后就会从range之后的数值开始下载,直到文件下载结束。
    请求头Range 格式:
    Range: bytes=start-end
    Range: bytes=10- :第10个字节及最后个字节的数据
    Range: bytes=40-100 :第40个字节到第100个字节之间的数据
    Range: bytes= 100-900,10000-20000:支持多个字节之间的数据

    这里大家应该可以理解为什么在服务器没有Accept-Range的情况下我们无法正确的下载文件了,因为文件在每次开始下载之后会从头开始下载直到结束。

    NSURLSession的使用

    NSURLSession是现在苹果推荐使用的用来进行网络请求的类,其封装更加完善,使用起来更加方便。
    在这篇文章中我将不对NSURLSession的具体使用做过多的说明,只放出我写的代码,通过代码大家可以对NSURLSession的使用有一个初步的认识。

    @interface SessionViewController ()<NSURLSessionDownloadDelegate>
    @property (weak, nonatomic) IBOutlet UIProgressView *progressView;
    @property (weak, nonatomic) IBOutlet UILabel *progressLabel;
    
    @property (strong,nonatomic) NSURLSession *session;
    @property (strong,nonatomic) NSURLSessionDownloadTask *downloadTask;
    @property (strong,nonatomic) NSData *resumeData;
    @property (assign,nonatomic) BOOL isResume;
    
    @end
    
    @implementation SessionViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        self.progressLabel.textAlignment = NSTextAlignmentCenter;
    }
    
    - (void)didReceiveMemoryWarning {
        [super didReceiveMemoryWarning];
        // Dispose of any resources that can be recreated.
    }
    
    
    - (NSURLSession *)session {
        if (!_session) {
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            _session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:[NSOperationQueue mainQueue]];
           
        }
        
        return _session;
    }
    
    
    - (IBAction)btnClickHandle:(id)sender {
        UIButton *btn = (UIButton *)sender;
        
        switch (btn.tag) {
            case 100:
                [self startDownloadFile];
                break;
            case 101:
                [self stopDownloadFile];
                break;
            case 102:
                [self cancelDownloadFile];
                break;
            default:
                break;
        }
    }
    
    
    - (void)startDownloadFile {
        NSString *urlStr = @"http://120.25.226.186:32812/resources/videos/minion_02.mp4";
        NSURL *url = [NSURL URLWithString:urlStr];
        if (_isResume) {
            self.downloadTask = [ self.session downloadTaskWithResumeData:self.resumeData];
            self.isResume = NO;
        }else {
             self.downloadTask = [self.session downloadTaskWithURL:url];
        }
       [_downloadTask resume];
    }
    
    - (void)stopDownloadFile {
        __weak typeof(self) weakSelf = self;
    //暂停下载
        [_downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
            weakSelf.resumeData = resumeData;
            weakSelf.downloadTask = nil;
            weakSelf.isResume = YES;
        }];
    }
    
    - (void)cancelDownloadFile {
        [_downloadTask cancel];
    }
    
    #pragma  mark -- NSURLSessionDownloadDelegate
    //下载完成后调用,获取最终的文件下载地址
    - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
        NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
        NSString *filePath = [caches stringByAppendingPathComponent:downloadTask.response.suggestedFilename];
        NSLog(@"File Path: %@",filePath);
        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSError *error = nil;
        [fileManager removeItemAtPath:filePath error:nil];
    //通过移动文件最终的下载地址将系统默认的下载内容移动到我们设置的文件位置
        [fileManager moveItemAtPath:location.path toPath:filePath error:&error];
        
        if (error) {
            NSLog(@"File Store Error:%@",error.localizedDescription);
        }
    }
    
    - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes {
    //    if (_isResume) {
    //        self.progressView.progress = (double) fileOffset / expectedTotalBytes;
    //        self.progressLabel.text = [NSString stringWithFormat:@"%.2f / 100",(double) fileOffset / expectedTotalBytes * 100];
    //    }
       
        
       
    }
    //下载过程中多次调用,获得已经下载的数据和一共需要下载的数据
    - (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
        self.progressView.progress = (double) totalBytesWritten / totalBytesExpectedToWrite;
        self.progressLabel.text = [NSString stringWithFormat:@"%.2f / 100",(double) totalBytesWritten / totalBytesExpectedToWrite * 100];
    }
    

    通过以上的代码我们会发现使用NSURLSession不需要对文件进行拼接操作,NSURLSession会自动帮助我们将文件下载拼接到系统指定的目录下。我们需要做的只是将默认路径下的内容移动到我们指定的目录下。

    NSURLConnection封装后实现多个任务的断点续传下载

    目录结构.png

    这个是我封装之后的目录结构,具体代码大家可以下载之后看一下,有很多幼稚的地方,算是抛砖引玉吧。
    在这里讲一下实现思路
    1,NSURLConnectionDownloader这个类是实现下载的具体实现类,在这里进行下载的具体操作。
    2,NSURLConnectionManager是对多个任务的管理类,在这里使用队列进行任务管理。
    3,使用Block回调的方式进行类之间的参数传递。

    以上就是现阶段已经完成的内容了,后续会对NSURLSession进行封装然后实现多任务断点续传。在这里我很想实现一个多线程下载的程序,所以如果有朋友有这方面的经验欢迎交流!

    如果你觉得这篇文章对你有用希望你能点赞啊!

    相关文章

      网友评论

        本文标题:浅谈iOS多任务断点下载

        本文链接:https://www.haomeiwen.com/subject/fthibttx.html