下載管理器

1、架構(gòu)

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


WechatIMG1.jpeg

當(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)題

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

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

  • 序言 在做項(xiàng)目的時(shí)候經(jīng)常會(huì)用到單文件下載或者批量文件的下載,并且需要實(shí)現(xiàn)斷點(diǎn)續(xù)傳,狀態(tài)變更,進(jìn)度回調(diào)等邏輯。本篇為...
    Colleny_Z閱讀 3,362評(píng)論 4 1
  • 前言 SDWebImage源碼閱讀1——整體脈絡(luò)結(jié)構(gòu)SDWebImage源碼閱讀2——緩存機(jī)制前兩篇研究了SDWe...
    Wang66閱讀 635評(píng)論 4 4
  • 日常扯蛋 從小到大都是被追求或是互相默許,前兩天第一次像個(gè)初中生,鼓起勇氣的,用微信對(duì)她承認(rèn)了“我的確是喜歡你!”...
    凌音同學(xué)閱讀 748評(píng)論 3 2
  • 0x00 寫(xiě)在前面 需求是實(shí)現(xiàn)一個(gè)下載管理器(實(shí)現(xiàn)了斷點(diǎn)續(xù)傳功能)項(xiàng)目是寫(xiě)過(guò)來(lái)個(gè)下載管理的類(lèi),更新界面使用的是KV...
    LeeYZ閱讀 1,208評(píng)論 3 9
  • 在這里需要兩對(duì)文件 1.在第一個(gè).h文件內(nèi)寫(xiě)兩個(gè)Block參數(shù)和一個(gè)代理協(xié)議 第一個(gè)參數(shù):正在下載 typedef...
    勝利_Soley閱讀 391評(píng)論 0 0

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