浅谈iOS多任务断点下载

前言

在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进行封装然后实现多任务断点续传。在这里我很想实现一个多线程下载的程序,所以如果有朋友有这方面的经验欢迎交流!

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,254评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,875评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,682评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,896评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,015评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,152评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,208评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,962评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,388评论 1 304
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,700评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,867评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,551评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,186评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,901评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,142评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,689评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,757评论 2 351

推荐阅读更多精彩内容