前言
在iOS開發(fā)當(dāng)中,文件的下載是經(jīng)常需要用到的一個(gè)功能,尤其是大文件的斷點(diǎn)下載。眾所周知,蘋果為開發(fā)者提供了兩個(gè)比較好用的原生處理網(wǎng)絡(luò)請求的類:
1,NSURLConnection
2,NSURLSession
當(dāng)然在GitHub這個(gè)世界上最大的同性交友網(wǎng)站??,你還可以找到很多處理網(wǎng)絡(luò)請求的第三方庫,例如AFNetWorking等 ...
這篇文章將主要介紹如何使用NSURLConnection和NSURLSession去實(shí)現(xiàn)斷點(diǎn)下載,并且我自己使用NSURLConnection封裝了一個(gè)處理多個(gè)下載任務(wù)的斷點(diǎn)下載庫,這個(gè)庫現(xiàn)在主要支持以下功能:
1,支持多任務(wù)下載管理
2,支持?jǐn)帱c(diǎn)下載
3,支持后臺(tái)下載
文中所使用的所有Demo和已經(jīng)封裝好的庫都在GitHub: JXDataTransimission
Talk is cheap, show me your demo!

Http淺談
上面的Demo可能會(huì)讓大家感到疑惑,為什么后兩個(gè)下載下來的文件會(huì)比文件實(shí)際大小要大呢?是不是代碼寫的有問題?
如果你對于Http有一定的了解那么你會(huì)知道,我們在向服務(wù)器發(fā)送Http請求之后,服務(wù)器會(huì)給我們一個(gè)響應(yīng),在響應(yīng)頭中包含一些服務(wù)器信息,其中有兩個(gè)內(nèi)容是我們這次需要特別注意的:
1,Accept-Range:說明服務(wù)器是否支持range設(shè)置
2,Content-Length:說明我們這次所請求的內(nèi)容總長度
那么讓我們來看一下以上其中兩個(gè)服務(wù)器響應(yīng)頭的內(nèi)容吧:


從上面的圖片我們可以清楚的看到響應(yīng)頭的內(nèi)容,其中一個(gè)是不支持Range的。
其實(shí)在寫這個(gè)demo之前我看了有些文章談到了range的用法,當(dāng)我發(fā)現(xiàn)其中有下載鏈接不能實(shí)現(xiàn)正確的斷點(diǎn)續(xù)傳之后我一度懷疑是自己的代碼有問題,之后通過Google發(fā)現(xiàn)其實(shí)只是服務(wù)器不支持range而已。??這也許就是一個(gè)菜鳥的辛酸??吧,很多時(shí)候看到別人寫的東西只知其然,不知其所以然。
NSURLConnection的使用
很多朋友會(huì)說,蘋果已經(jīng)停止了對NSURLConnection的支持,我們沒有必要再了解這個(gè)類了,這個(gè)說法沒有錯(cuò),但是我認(rèn)為如果想對于下載過程有一個(gè)更好的認(rèn)識(shí),我們最好還是自己去寫一個(gè)NSURLConnection的下載,因?yàn)橄鄬τ贜SURLSession的強(qiáng)大封裝,NSURLConnection需要我們自己實(shí)現(xiàn)下載的細(xì)節(jié)內(nèi)容,因此也有助于我們理解斷點(diǎn)續(xù)傳的邏輯。
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請求對象下載內(nèi)容,它可以提供URL請求的異步下載,我們可以自己配置URL 請求對象,以此來實(shí)現(xiàn)下載。
我們使用的NSURLConnection主要方法和代理
在這個(gè)Demo中我們使用了以下方法:


以上的方法注釋已經(jīng)很清楚了,在這里我想說的是一個(gè)問題:
若果我們使用-sendAsynchronousRequest:queue:completionHandler:方法創(chuàng)建NSURLConnection對象的話,那么只有當(dāng)下載完成的時(shí)候才能獲得block回掉,它返回的信息十分有限,我們不能通過這個(gè)方法查看下載過程中的情況。
因此,我們需要使用設(shè)置代理的其它幾個(gè)方法去創(chuàng)建NSURLConnection對象。
接下來讓我們看一下代理吧。
主要使用了兩個(gè)代理:
1,NSURLConnectionDelegate: 主要處理鏈接時(shí)的一些設(shè)置和請求。
2, NSURLConnectionDataDelegate:主要處理數(shù)據(jù)傳輸中的重要信息。
在我上傳的Demo中有一個(gè)視圖控制器叫做ConnectionViewController,在其中實(shí)現(xiàn)了NSURLConnection的斷點(diǎn)續(xù)傳下載。

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
//當(dāng)鏈接建立之后,服務(wù)器會(huì)向客戶端發(fā)送響應(yīng),此時(shí)這個(gè)代理方法會(huì)被自動(dòng)調(diào)用,只調(diào)用一次。
- (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);
}
}
//當(dāng)客戶端收到數(shù)據(jù)之后,會(huì)調(diào)用這個(gè)方法,這個(gè)方法將被多次調(diào)用
- (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];
}
//當(dāng)下載結(jié)束之后調(diào)用這個(gè)方法
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
[self.connection cancel];
self.connection = nil;
[_fileHandle closeFile];
self.fileHandle = nil;
NSLog(@"Length After Loading:%lld",_currentLength);
}
代碼比較容易理解,在這里只說兩個(gè)需要注意的地方
1,使用fileHandle來確定每次數(shù)據(jù)需要寫入本地文件的位置,通過這個(gè)辦法將斷點(diǎn)續(xù)傳之后下載的數(shù)據(jù)寫入本地文件的正確位置。
2,通過設(shè)置NSURLMutableRequest的請求頭來改變r(jià)ange的值,這樣每次斷點(diǎn)之后就會(huì)從range之后的數(shù)值開始下載,直到文件下載結(jié)束。
請求頭Range 格式:
Range: bytes=start-end
Range: bytes=10- :第10個(gè)字節(jié)及最后個(gè)字節(jié)的數(shù)據(jù)
Range: bytes=40-100 :第40個(gè)字節(jié)到第100個(gè)字節(jié)之間的數(shù)據(jù)
Range: bytes= 100-900,10000-20000:支持多個(gè)字節(jié)之間的數(shù)據(jù)
這里大家應(yīng)該可以理解為什么在服務(wù)器沒有Accept-Range的情況下我們無法正確的下載文件了,因?yàn)槲募诿看伍_始下載之后會(huì)從頭開始下載直到結(jié)束。
NSURLSession的使用
NSURLSession是現(xiàn)在蘋果推薦使用的用來進(jìn)行網(wǎng)絡(luò)請求的類,其封裝更加完善,使用起來更加方便。
在這篇文章中我將不對NSURLSession的具體使用做過多的說明,只放出我寫的代碼,通過代碼大家可以對NSURLSession的使用有一個(gè)初步的認(rèn)識(shí)。
@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
//下載完成后調(diào)用,獲取最終的文件下載地址
- (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];
//通過移動(dòng)文件最終的下載地址將系統(tǒng)默認(rèn)的下載內(nèi)容移動(dòng)到我們設(shè)置的文件位置
[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];
// }
}
//下載過程中多次調(diào)用,獲得已經(jīng)下載的數(shù)據(jù)和一共需要下載的數(shù)據(jù)
- (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];
}
通過以上的代碼我們會(huì)發(fā)現(xiàn)使用NSURLSession不需要對文件進(jìn)行拼接操作,NSURLSession會(huì)自動(dòng)幫助我們將文件下載拼接到系統(tǒng)指定的目錄下。我們需要做的只是將默認(rèn)路徑下的內(nèi)容移動(dòng)到我們指定的目錄下。
NSURLConnection封裝后實(shí)現(xiàn)多個(gè)任務(wù)的斷點(diǎn)續(xù)傳下載

這個(gè)是我封裝之后的目錄結(jié)構(gòu),具體代碼大家可以下載之后看一下,有很多幼稚的地方,算是拋磚引玉吧。
在這里講一下實(shí)現(xiàn)思路
1,NSURLConnectionDownloader這個(gè)類是實(shí)現(xiàn)下載的具體實(shí)現(xiàn)類,在這里進(jìn)行下載的具體操作。
2,NSURLConnectionManager是對多個(gè)任務(wù)的管理類,在這里使用隊(duì)列進(jìn)行任務(wù)管理。
3,使用Block回調(diào)的方式進(jìn)行類之間的參數(shù)傳遞。