1、架構(gòu)
項(xiàng)目中需要管理下載,多個(gè)任務(wù)同時(shí)下載,下載的暫停,恢復(fù),進(jìn)度顯示等等。比如現(xiàn)在的百度云,迅雷等軟件的下載功能:

當(dāng)然我不要做的那么牛逼,但是在現(xiàn)有的基礎(chǔ)上還是可以模仿一個(gè)差不多的下載管理器出來(lái)的。我主要用的下面三個(gè)類(lèi):
KTDownloadManager.h
KTDownloadManager.m
KTDownloadModel.h
KTDownloadModel.m
KTDownloadOperation.h
KTDownloadOperation.m
主要有下面的功能:
1、添加,刪除下載項(xiàng);
2、開(kāi)始、暫停下載;
3、進(jìn)度顯示,錯(cuò)誤提醒,保存路徑等;
4、斷點(diǎn)續(xù)傳;
5、下載項(xiàng)的本地序列化,反序列化(對(duì)于可以斷點(diǎn)續(xù)傳的下載項(xiàng),殺掉應(yīng)用后還可以繼續(xù)下載);
1.1、KTDownloadManager
使用單例方法生成的實(shí)例來(lái)做下載管理者,他負(fù)責(zé)管理KTDownloadModel和調(diào)度KTDownloadOperation,像SDWebImage也是使用的單例來(lái)管理下載的。除了session之外,KTDownloadManager還有兩個(gè)重要的屬性:
// 下載隊(duì)列,默認(rèn)并發(fā)下載數(shù)量為3
@property (nonatomic, strong, readonly) NSOperationQueue *downloadQueue;
// download models
@property (nonatomic, strong) NSMutableArray *downloadModels;
// nsurlsession
@property (nonatomic, strong) NSURLSession *session;
這兩個(gè)都是私有屬性,因?yàn)椴恍枰饷嬷馈ownloadModels是所有的KTDownloadModel對(duì)象的集合。downloadQueue是下載隊(duì)列,實(shí)際下載操作在KTDownloadOperation里面進(jìn)行,那么下載過(guò)程就是添加KTDownloadOperation實(shí)例到downloadQueue中。
1.2、KTDownloadModel
這里的model是用來(lái)顯示以及保存相關(guān)信息的,比如界面上有一個(gè)下載項(xiàng)就有一個(gè)model,model里面有基本的url,totalReceivedBytes,totalBytes等屬性,表征一個(gè)下載的基本信息。我們?cè)谧鼋缑骘@示的時(shí)候,只需要把model的屬性展示出來(lái)即可,下載進(jìn)度等等變化也只需要監(jiān)聽(tīng)對(duì)應(yīng)的model屬性變化,這里使用代理來(lái)通知界面UI變化。
@interface KTDownloadModel : NSObject
// 下載的url
@property (nonatomic, strong, readonly) NSURL *url;
// 下載的文件全路徑,可以指定,必須處于Documents或者Library/Caches文件夾下面
// 如果為nil,那么下載完成之后使用KTDownloadManager的downloadFolderPath配合suggest name構(gòu)成文件全路徑
@property (nonatomic, copy) NSString *downloadFilePath;
// 已接收的總字節(jié)數(shù)
@property (nonatomic, assign) int64_t totalReceivedBytes;
// 當(dāng)前接收的data,由于存在斷點(diǎn)續(xù)傳,只表示這一次下載的data,并不表示下載的總data
@property (nonatomic, strong) NSMutableData *receivedData;
// 總字節(jié)數(shù)
@property (nonatomic, assign) int64_t totalBytes;
// 下載狀態(tài)
@property (nonatomic, assign) KTDownloadState state;
// 下載operation,正在下載中或者暫停一段時(shí)間之內(nèi)的model會(huì)有一個(gè)operation,其他情況為nil
@property (nonatomic, weak) KTDownloadOperation *operation;
// delegate
@property (nonatomic, weak) id<KTDownloadModelDelegate> delegate;
// error
@property (nonatomic, strong) NSError *error;
1.3、KTDownloadOperation
實(shí)際下載動(dòng)作都在KTDownloadOperation里面進(jìn)行,這里是模仿SDWebImage的下載隊(duì)列來(lái)的,使用NSOperationQueue的好處是你可以設(shè)置同時(shí)下載的最大個(gè)數(shù),同時(shí)你暫停一個(gè)下載之后,或者一個(gè)下載完成之后下一個(gè)下載會(huì)自動(dòng)開(kāi)始,還有可以很方便的暫停,啟動(dòng),取消一個(gè)下載操作,這些都非常符合下載隊(duì)列的需求,而GCD是做不到的。
@interface KTDownloadOperation : NSOperation
// 每個(gè)operation必須有一個(gè)model
@property (nonatomic, weak, readonly) KTDownloadModel *downloadModel;
@property (nonatomic, strong, readonly) NSURLSessionDataTask *dataTask;
1.4、三者之間的關(guān)系
KTDownloadManager是管理KTDownloadModel和KTDownloadOperation的,model負(fù)責(zé)記錄下載項(xiàng)的進(jìn)度,url等屬性,同時(shí)和UI打交道,operation負(fù)責(zé)下載。下載操作就是KTDownloadManager通過(guò)model生成一個(gè)operation操作,然后把它丟到隊(duì)列queue中去下載,下載進(jìn)度,結(jié)果,錯(cuò)誤等operation會(huì)告訴model,然后model在記錄下來(lái)的同時(shí)會(huì)去通知UI。model和operation會(huì)互相弱引用,由于一個(gè)下載項(xiàng)對(duì)應(yīng)一個(gè)model,但是下載項(xiàng)不一定處于下載狀態(tài),有可能暫停,沒(méi)開(kāi)始,或者已經(jīng)完成,此時(shí)是沒(méi)有對(duì)應(yīng)一個(gè)operation的。所以KTDownloadModel的operation屬性可能為空,但是KTDownloadOperation的downloadModel屬性一定不為空。
2、注意點(diǎn)
2.1、使用NSURLSessionDataTask
我沒(méi)用NSURLSessionDownloadTask的原因居然是斷點(diǎn)續(xù)傳,實(shí)際上NSURLSessionDownloadTask下載的時(shí)候會(huì)將臨時(shí)下載的文件保存在tmp文件夾中,下一次下載的時(shí)候可以根據(jù)這個(gè)文件恢復(fù)下載,即實(shí)現(xiàn)斷點(diǎn)續(xù)傳。但是殺掉應(yīng)用之后,tmp文件夾很有可能被清掉,那么此時(shí)是不能恢復(fù)下載的。當(dāng)然也可以像這兩篇文章那樣曲線(xiàn)救國(guó)實(shí)現(xiàn)退出應(yīng)用后的斷點(diǎn)續(xù)傳:
http://www.cocoachina.com/ios/20160503/16053.html
http://www.tuicool.com/articles/uyQrIzi
我采用的辦法是直接使用NSURLSessionDataTask來(lái)下載,自己實(shí)現(xiàn)臨時(shí)文件的管理以及恢復(fù)下載的邏輯。實(shí)現(xiàn)這兩個(gè)代理方法:
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data
{
self.downloadModel.totalBytes = [dataTask.response expectedContentLength];
[self.receivedData appendData:data];
dispatch_async(dispatch_get_main_queue(), ^{
self.downloadModel.totalReceivedBytes += data.length;
});
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error
{
......
}
自己管理下載的臨時(shí)數(shù)據(jù),自己保存臨時(shí)數(shù)據(jù)。
2.2、斷點(diǎn)續(xù)傳
http斷點(diǎn)續(xù)傳使用http請(qǐng)求頭Range字段來(lái)實(shí)現(xiàn)的,網(wǎng)上有很多文章:
http://www.cnblogs.com/ziyunfei/archive/2012/11/18/2775499.html
KTDownloadOperation有一個(gè)私有屬性receivedData用來(lái)保存當(dāng)前下載的內(nèi)容,如果你暫停了下載,或者其他原因斷掉了,KTDownloadOperation會(huì)做下面的操作:
1、檢查返回的response(也就是http響應(yīng))頭部信息里面有沒(méi)有Accept-Ranges字段,并且值不是none。比如http響應(yīng)頭里有這樣的字段Accept-Ranges: bytes,那么說(shuō)明服務(wù)器支持Range分段請(qǐng)求,否則不支持。
2、如果支持?jǐn)帱c(diǎn)續(xù)傳那么保存當(dāng)前下載的receivedData到本地。
啟動(dòng)下載的時(shí)候,根據(jù)receivedData的大小來(lái)設(shè)置http請(qǐng)求頭Range:bytes=1024-,假設(shè)receivedData的大小是1024字節(jié)。
2.3、文件size的返回
我在測(cè)試的時(shí)候發(fā)現(xiàn)有些鏈接,比如github上面的這個(gè)下載鏈接:
https://codeload.github.com/hanton/HTY360Player/zip/master
下載時(shí)不能正確知道Content-Length的大小,不知道這個(gè)就等于你沒(méi)法顯示下載進(jìn)度,這里有解決辦法:
http://stackoverflow.com/questions/12235617/mbprogresshud-with-nsurlconnection/12599242#12599242
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:anURL];
[request addValue:@"" forHTTPHeaderField:@"Accept-Encoding"];
此時(shí)服務(wù)器就會(huì)在http響應(yīng)頭里面寫(xiě)上正確的Content-Length字段值了。
2.4、持久化
我這里就是用的很簡(jiǎn)單的plist文件保存。KTDownloadManager監(jiān)聽(tīng)applicationWillTerminate消息,然后將當(dāng)前下載項(xiàng)保存,下次啟動(dòng)讀取plist文件即可。
2.5、NSURLSession代理回調(diào)的分發(fā)
NSURLSession對(duì)象是KTDownloadManager的屬性,NSURLSession的代理也是KTDownloadManager,那么上面提到的回調(diào)方法只能寫(xiě)在KTDownloadManager里面。但是有多個(gè)下載操作,NSURLSession的代理回調(diào)內(nèi)容必須正確分發(fā)到KTDownloadOperation中。這里模仿的是AFNetworking的做法,使用分類(lèi)給dataTask添加屬性downloadOperation,從而讓dataTask與operation一一對(duì)應(yīng),KTDownloadOperation復(fù)寫(xiě)代理方法即可。
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data
{
[dataTask.downloadOperation URLSession:session dataTask:dataTask didReceiveData:data];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error
{
[task.downloadOperation URLSession:session task:task didCompleteWithError:error];
}
2.6、UI更新
給KTDownloadModel設(shè)置一個(gè)state屬性,表明下載狀態(tài),通過(guò)這兩個(gè)代理方法告訴UI下載狀態(tài)和進(jìn)度的變化:
typedef NS_ENUM(NSUInteger, KTDownloadState)
{
KTDownloadStateNone = 0, // 創(chuàng)建新的實(shí)例時(shí)所處狀態(tài)
KTDownloadStateWaiting, // 等待中(前面還有正在下載的操作)
KTDownloadStateDownloading, // 下載中
KTDownloadStatePaused, // 暫停
KTDownloadStateFinished, // 完成
KTDownloadStateFailed // 失敗
};
@protocol KTDownloadModelDelegate <NSObject>
// 以下代理方法在operation存在并且在下載的時(shí)候才會(huì)調(diào)用
@optional
- (void)downloadModel:(KTDownloadModel *)model didChangedState:(KTDownloadState)state;
- (void)downloadModel:(KTDownloadModel *)model didReceivedTotalBytes:(int64_t)totalReceivedBytes totalBytes:(int64_t)totalBytes;
@end
如果只是在一個(gè)靜態(tài)頁(yè)面顯示一個(gè)KTDownloadModel下載項(xiàng)這樣并不會(huì)有什么問(wèn)題,但是如果使用tableView顯示多個(gè)下載項(xiàng)就會(huì)出問(wèn)題了。比如文章開(kāi)頭的那個(gè)圖里面,一個(gè)tableViewCell顯示一個(gè)下載項(xiàng),我們肯定會(huì)這樣寫(xiě):
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
DownloadTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"DownloadTableViewCell" forIndexPath:indexPath];
KTDownloadModel *model = [self.downloadModels objectAtIndex:indexPath.row];
model.delegate = cell;
[cell configWithModel:model];
return cell;
}
這樣基本是沒(méi)什么問(wèn)題的,但是我們都知道tableView會(huì)復(fù)用tableViewCell,那么在滑動(dòng)頁(yè)面的時(shí)候,會(huì)出現(xiàn)這樣的情況:比如有20個(gè)KTDownloadModel需要展示,但是一個(gè)界面只能展示10個(gè)tableViewCell,那么tableView實(shí)際上會(huì)有11個(gè)tableViewCell實(shí)例在內(nèi)存里。tableViewCell1展示KTDownloadModel1,tableViewCell2展示KTDownloadModel2。。。tableViewCell11展示KTDownloadModel11?;降?2個(gè)的時(shí)候,此時(shí)是tableViewCell1來(lái)展示KTDownloadModel12的。說(shuō)了這么多就是想說(shuō)同一個(gè)tableViewCell可能會(huì)在不同的時(shí)候展示不同的KTDownloadModel,上面的代碼會(huì)導(dǎo)致同一個(gè)tableViewCell是多個(gè)KTDownloadModel的代理,那么意味著一個(gè)tableViewCell可能在同一個(gè)時(shí)刻收到多個(gè)KTDownloadModel的進(jìn)度或狀態(tài)更新通知,那么就會(huì):顯示紊亂!
要保證tableViewCell在一個(gè)時(shí)刻只能是一個(gè)KTDownloadModel的代理,我在KTDownloadManager里面添加了這個(gè)方法:
- (void)setDelegate:(id<KTDownloadModelDelegate>)delegate forModel:(KTDownloadModel *)model
{
for (KTDownloadOperation *op in self.downloadQueue.operations) {
if (op.downloadModel.delegate == delegate) {
op.downloadModel.delegate = nil;
}
}
model.delegate = delegate;
}
將上面的model.delegate = cell替換成這一句:[[KTDownloadManager sharedManager] setDelegate:cell forModel:model];保證一個(gè)cell同一時(shí)刻只能是一個(gè)model的代理,就不會(huì)出現(xiàn)顯示紊亂的問(wèn)題了。
3、完善
項(xiàng)目地址:https://github.com/tujinqiu/KTDownloadManager
1、使用文件句柄寫(xiě)緩存,避免內(nèi)存占用過(guò)大
2、后臺(tái)下載
3、bug修復(fù)
歡迎提問(wèn)題