?轉自DeveloperLx的github ? 鏈接:DeveloperLx/Troubles-of-realizing-download-module: 實現(xiàn)項目下載需求時遇過的那些坑
導語
當前市面上的APP宣蔚,凡有涉及到視頻霜旧、期刊妻熊、或其它大型文件傳輸宙拉、瀏覽等用途的,添加下載或緩存至本地的功能以避免網(wǎng)速的限制及依賴,毫無疑問都將給用戶帶來更好的體驗。而談到下載技術,就又不得不牽扯到了斷點續(xù)傳笔刹,隊列任務等老生常談的問題。這不冬耿,本人當前的項目舌菜,就恰好遇到了這樣的需求。然而在經過大量調研之后亦镶,本人竟無法找到一篇總結得很好的文檔日月,對此進行全面的介紹袱瓮;能夠尋到的一些活躍度并不高的開源項目,卻又不能恰如其分并抱之以信賴滿足項目的需求爱咬。所以仔細斟酌后尺借,不得不選擇自己動手,豐衣足食精拟。鉆研的過程中遇到了不少坑燎斩、不少困難,有些個別的地方真是不用不知道蜂绎,一用才知道是如此得蹩腳栅表,難怪很少有人對此進行系統(tǒng)完整的介紹。現(xiàn)將本人在實現(xiàn)下載模塊時所用到的技術總結如下师枣,相信本文中所蘊涵的干貨一定不會令費心閱讀的你感到失望怪瓶!
話休絮煩。首先坛吁,說下載就離不開網(wǎng)絡請求。而當今iOS開發(fā)技術當中铐尚,最廣泛使用的網(wǎng)絡請求框架無疑要屬AFNetworking拨脉。經過對其進行簡單研究后,你就會尋到最適合用來完成下載這件“小事”的組件宣增,叫做AFHTTPRequestOperation
現(xiàn)假定我們的需求是最常見玫膀,也是最能體現(xiàn)技術問題的一個,叫做:
下載隊列在某一時刻爹脾,最多僅能有一個下載任務處于正在下載的狀態(tài)中帖旨!
-- 敘述的節(jié)奏似乎稍稍快了些
那就先來看下實現(xiàn)隊列下載、斷點續(xù)傳等需求的關鍵示例代碼吧!
NSError * error = nil;
// 創(chuàng)建下載隊列
NSOperationQueue * downloadOperationQueue = [[NSOperationQueue alloc]init];
// ?規(guī)定operationQueue中灵妨,最大可以同時執(zhí)行的operation數(shù)量為1
downloadOperationQueue.maxConcurrentOperationCount = 1;
// 創(chuàng)建單個下載任務(訪問已下載部分的文件解阅,實現(xiàn)斷點續(xù)傳)
NSMutableURLRequest * downloadRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:DOWNLOAD_URL_STRING]];
[[NSURLCache sharedURLCache] removeCachedResponseForRequest:downloadRequest];
AFHTTPRequestOperation * downloadOperation = [[AFHTTPRequestOperation alloc]initWithRequest:downloadRequest];
unsigned long long downloadedPartFileSize = 0;
if ([[NSFileManager defaultManager] fileExistsAtPath:DOWNLOADED_PART_FILE_PATH]) {
NSDictionary * fileAttributes = [[NSFileManager defaultManager]attributesOfItemAtPath:DOWNLOADED_PART_FILE_PATH error:&error];
downloadedPartFileSize = [fileAttributes fileSize];
NSString * headerRangeFieldValue = [NSString stringWithFormat:@"bytes=%llu-", downloadedPartFileSize];
[downloadRequest setValue:headerRangeFieldValue forHTTPHeaderField:@"Range"];
}
downloadOperation.outputStream = [NSOutputStream outputStreamToFileAtPath:DOWNLOADED_PART_FILE_PATH append:YES];
[downloadOperation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
NSLog(@"%lld/%lld", totalBytesRead + downloadedPartFileSize, totalBytesExpectedToRead + downloadedPartFileSize);
}];
[downloadOperation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(@"downloadOperation completion block invoked");
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(@"downloadOperation failure block invoked");
}];
// ?將單個下載任務加入到下載隊列當中
[downloadOperationQueue addOperation:downloadOperation];
// ?暫停某下載任務
[downloadOperation pause];
// ?繼續(xù)某下載任務
[downloadOperation resume];
// ?取消某下載任務(同時應將其已下載部分的文件刪除)
[downloadOperation cancel];
[[NSFileManager defaultManager] removeItemAtPath:DOWNLOADED_PART_FILE_PATH error:&error];
// ?取消全部下載任務
[downloadOperationQueue cancelAllOperations];
// ?此外還有若干方法用以判斷相應一見其名便知其義的狀態(tài)...
downloadOperation.isReady
downloadOperation.isExecuting
downloadOperation.isPaused
downloadOperation.isCancelled
downloadOperation.isFinished
// ?判斷downloadOperation是否存在在downloadOperationQueue當中
[downloadOperationQueue.operations containsObject:downloadOperation]
以上代碼創(chuàng)建一個AFHTTPRequestOperation對象作為單個下載任務,并將其加入到NSOperationQueue中泌霍。仿照上例货抄,我們可以創(chuàng)建多個AFHTTPRequestOperation對象并加入到NSOperationQueue中,即形成了下載隊列
做到這里朱转,你是不是認為已經沒有神馬技術問題啦蟹地?只要把operation一個個地添加到queue里, 下載任務就可以一個接一個地自動執(zhí)行了!而如果我們將上一個operation暫停藤为、取消怪与,或是它自然地下載完成了,又或是它下載中途失敗了缅疟,下一operation就會聰明地自動啟動分别,繼續(xù)其下載任務了1樵浮!茎杂?
錯4砝馈!;屯倾哺!
接下來筆者將要告訴你的,就是本文最最核心的干貨刽脖,絕對顛覆你的想象P吆!!
只要你親手動手試一試曲管,就會發(fā)現(xiàn)如下大跌眼球的驚恐現(xiàn)象H吹恕!
驚人事實 1: 對queue中前一個下載operation執(zhí)行pause方法院水,下一個operation并不能自動啟動進入正在執(zhí)行的狀態(tài)@搬恪!
驚人事實 2: 如果queue中前一個下載operation執(zhí)行失敗了(可用下載中途斷網(wǎng)進行模擬)檬某,它將從queue中自動地被移除掉G颂凇!
驚人事實 3: 注意到代碼里那個failure回調的block了沒恢恼?它不僅僅將在operation執(zhí)行失敗的時候被調用民傻,還會在operation被cancel的時候被調用!场斑!所以對于神馬叫做“operation的失敗”漓踢,你要重新建立起你的世界觀了!漏隐!
驚人事實 4: 如果對一個正處于pause狀態(tài)的operation執(zhí)行cancel會怎么樣喧半?答案是這個operation還保留在queue中!青责!并且仍然保持著pause狀態(tài)J碓汀!僅有的一點變化爽柒,是它的isCancelled屬性吴菠,變成了YES!浩村!
......未完待續(xù)做葵,本文要令你感到驚詫的,還有很多
由于這些問題間相互關系的錯綜復雜心墅,為了清晰條理地予以說明酿矢,特將本人實驗中所觀察到operation的行為總結如下表榨乎。其中有悖于我們想象的結果,已用彩色背景字體標出
有木有感到AFHTTPRequestOperation和NSOperationQueue是個多么坑爹的東東瘫筐?為何就不能像我們想象中一樣用得舒爽蜜暑?
原因就在于AFHTTPRequestOperation的父類NSOperation,在設計之處就不是為了下載的操作而生的策肝!人家開始就僅僅是用來處理多線程的案睾础!之众!所以造成了AFNetworking在擴展這個類的時候拙毫,可用的資源、接口等等就非常少棺禾。對于什么下載任務暫停/繼續(xù)缀蹄,下載中途失敗等等情況,很多問題幾乎就是沒有辦法理想地解決的膘婶,只好用NSOperation中僅有的幾種狀態(tài)予以并不貼切的表示缺前。于是乎就出現(xiàn)了上表中種種詭異的情況
補充幾點干貨。然后告訴你一個本文之前偷偷誤導了你的大坑P蟆衅码!
驚人事實 5:如果一個queue中有一個下載operation正在執(zhí)行,此時對另一處在isReady狀態(tài)的operation執(zhí)行start方法會怎么樣古胆?你很可能會說:“沒用的肆良,因為之前設了queue.maxConcurrentOperationCount = 1嘛筛璧!” 可事實恰好相反逸绎,這個operation也會立刻被啟動執(zhí)行!夭谤!于是乎你不忍心看到的事情就出現(xiàn)了棺牧,這時queue將會有兩個任務被同時執(zhí)行!朗儒!maxConcurrentOperationCount完全失效了<粘恕!
驚人事實 6:承接上一點醉锄,如果此時另一條的狀態(tài)不是isReady乏悄,而是isPaused暫停狀態(tài),你對其執(zhí)行resume方法恳不,此時會怎么樣呢檩小?哈哈,沒錯烟勋,你吸取了上一條的經驗规求,終于猜對了筐付!這個operation也會立刻啟動被執(zhí)行,不管當前的queue有沒有另一個operation正在被執(zhí)行W柚住瓦戚!從中我們就可以意識到,maxConcurrentOperationCount這個屬性丛塌,只能管得自動啟動每一operation時较解,先檢查下是否正在執(zhí)行的operation的數(shù)量已經超過那個數(shù)字了;可是如果你要手動start某一operation姨伤,對不起哨坪,這條限制半點都沒有用處了......
驚人事實 7:從上表中我們可以看到,無論是一個operation自然地執(zhí)行完畢乍楚,還是中途失敗当编,還是被執(zhí)行了cancel方法,都會被標記為isFinished徒溪,從operation中被移除掉忿偷,operation所認為的“完成”可完全不像我們想象中的那么狹義!問題來了臊泌,此時如果再對這個operation執(zhí)行start方法會怎么樣鲤桥?對不起!沒有任何用處渠概!所以你如果想要讓一個已失敗的operation從斷點處繼續(xù)再開始執(zhí)行下載該怎么辦茶凳?不好意思,只好新建operation重新再來了......
基于實驗我們又可以得出了這樣的一張流程圖:
頭痛播揪、抓狂得很爸!猪狈!本人剛開始實現(xiàn)下載模塊相關需求的時候箱沦,就被這些問題坑了個體無完膚。最后得出了本文最大的關鍵結論雇庙,也就是前面所說的“大坑”:
不能夠使用NSOperationQueue來進行多下載任務的管理N叫巍!疆前!
理由如下:
你無法妥善地實現(xiàn)“隊列中最多僅能有一個下載任務正在進行”這條產品經理臆測會讓開發(fā)變簡單的需求:!比方說竹椒,你讓
NSOperationQueue中一個operation暫停后童太,下一個任務并不會自動啟動啊!有人說可以手動去start下一個operation康愤,
如果這個姑且算做可以接受儡循,可是問題又來了:我們沒有辦法手動將一個operation置為isReady狀態(tài)啊U骼洹择膝!處于isReady狀態(tài)的
operation,要么是還未加入queue检激,要么是加入了還未輪到執(zhí)行肴捉,但是它只要一執(zhí)行,就再也回不到isReady的狀態(tài)了叔收!那我們要讓暫停的
operation恢復到等待下載狀態(tài)該怎么搞齿穗?此時可能還有另一operation正在執(zhí)行啊=嚷伞窃页!反之筆者搞了半天,是無能為力了
下載是需要一定時間的過程复濒,需要不停地向服務器進行請求脖卖,那么就永遠避免不了因為網(wǎng)絡等原因中途會失敗的問題∏删保可要命的是畦木,一旦下載失敗,operation就會毫不妥協(xié)地從queue中被移除掉霸曳骸J!你能在這時候讓你的下載任務從UI界面上消失掉嗎唇礁?顯然大BOSS是不會允許你這么干的勾栗。有人說可以重建operation再加入到queue中,可那樣你只能將operation插到隊尾垒迂,列表順序就被打亂了靶狄觥6噬摺机断!你去瞧瞧看,operationQueue.operations绣夺,那可只是一個只讀屬性袄艏椤!陶耍!
......自己去體會吧奋蔚,反正坑多的已經無力吐槽,再堅持下去也是枉費心思了。
不幸的事情來了泊碑。筆者最后只得放棄NSOperationQueue坤按,使用古老原始的工具--NSMutableArray來進行多下載任務的管
理。這樣的話所有operation的啟動馒过、移除等操作都必須依靠手動來執(zhí)行臭脓。這個辦法雖然辦法土了些,可是起碼對于每個operation的控制權又重
新回到了我們手里腹忽。有得必有失嘛来累!當能恰當?shù)貙崿F(xiàn)了項目需求的時候,這點犧牲也就算不上神馬了
在使用AFHTTPRequestOperation時我們還需要注意以下幾點:
對isReady狀態(tài)的operation執(zhí)行resume窘奏、pause嘹锁、cancel等方法是沒有任何用處的,所以為了確保執(zhí)行正確着裹,在對
operation執(zhí)行resume领猾、pause、cancel前骇扇,都要首先執(zhí)行[operation
start]瘤运。(對已經start過的operation執(zhí)行start不會造成任何影響)
對處于isPaused的operation執(zhí)行cancel方法是無法得到正確結果的,所以每次執(zhí)行cancel方法前匠题,都要先執(zhí)行一下
[operation resume]拯坟。
(同樣對于正處于isExecuting狀態(tài)的operation來說,執(zhí)行resume方法也是不會造成任何影響的)
對于下載模塊這個糾結之處來說韭山,本地持久化下載記錄的相關數(shù)據(jù)也是必不可少的郁季,理由如下:
a. ?AFHTTPRequestOperation、NSMutableArray這些都是運行時的東西钱磅,一關掉app梦裂,這些東西自然也都消失得無影無蹤了。我們能讓下載記錄就此消失得無影無蹤么盖淡?NO年柠!顯然是不能接受的
b. ?我們下載得到的那個文件,可能是已下載完成的褪迟,可能是只下載了部分的冗恨;而只下載了部分這種的,又可能是下載中途暫停了的味赃,失敗的掀抹,被取消的等等情況。請問單憑這個文件如何判斷它是屬于哪種情況心俗?而且這還不夠傲武,有些下載任務根本可能就還未生成相應的下載文件蓉驹,app就已經被關了啊揪利!你能就把這種的下載任務扔掉嗎态兴?顯然是絕不可以的
c. ?不使用operationQueue我們同樣無法手動將operation標記為隊列等待的isReady狀態(tài),怎么辦疟位?只有將operation設定為paused诗茎,然后相應的數(shù)據(jù)記錄標記為isReady狀態(tài)好了(本人使用的是CoreData進行本地持久化存儲)
d. ?......用operation外的數(shù)據(jù)模型記錄下載任務的狀態(tài)好處還有很多,但同時帶來的同步更新問題也有很多献汗,具體就留給大家自己去體會了敢订!
以上就是本人總結下載模塊實現(xiàn)時需要注意到的種種內容。當然各位大神如果有更好的方案提出罢吃,比如用本人掌握得還不夠好的stream如何實現(xiàn)上述需求楚午,本人也愿虛心聽取以將此處完善得更好。歡迎直言批評與不吝賜教D蛘小矾柜!
github還有續(xù)篇 ? 鏈接:github.com/DeveloperLx/Dreamy_download_manage_solution