淺談iOS多任務(wù)斷點(diǎn)下載

前言

在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!

點(diǎn)我下載.gif

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)容吧:

支持Range.png
不支持Range.png

從上面的圖片我們可以清楚的看到響應(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中我們使用了以下方法:

數(shù)據(jù)異步下載.png
取消下載.png

以上的方法注釋已經(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ù)傳下載。

NSURLConnection斷點(diǎn)續(xù)傳.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
//當(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ù)傳下載

目錄結(jié)構(gòu).png

這個(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ù)傳遞。

以上就是現(xiàn)階段已經(jīng)完成的內(nèi)容了,后續(xù)會(huì)對NSURLSession進(jìn)行封裝然后實(shí)現(xiàn)多任務(wù)斷點(diǎn)續(xù)傳。在這里我很想實(shí)現(xiàn)一個(gè)多線程下載的程序,所以如果有朋友有這方面的經(jīng)驗(yàn)歡迎交流!

如果你覺得這篇文章對你有用希望你能點(diǎn)贊??!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容