iOS實現(xiàn)項目下載需求時遇過的那些坑

?轉自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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市就谜,隨后出現(xiàn)的幾起案子怪蔑,更是在濱河造成了極大的恐慌,老刑警劉巖丧荐,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缆瓣,死亡現(xiàn)場離奇詭異,居然都是意外死亡虹统,警方通過查閱死者的電腦和手機弓坞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來车荔,“玉大人渡冻,你說我怎么就攤上這事∮潜悖” “怎么了族吻?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長珠增。 經常有香客問我超歌,道長,這世上最難降的妖魔是什么切平? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任握础,我火速辦了婚禮辐董,結果婚禮上悴品,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好苔严,可當我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布定枷。 她就那樣靜靜地躺著,像睡著了一般届氢。 火紅的嫁衣襯著肌膚如雪欠窒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天退子,我揣著相機與錄音岖妄,去河邊找鬼。 笑死寂祥,一個胖子當著我的面吹牛荐虐,可吹牛的內容都是我干的。 我是一名探鬼主播丸凭,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼福扬,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了惜犀?” 一聲冷哼從身側響起铛碑,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎虽界,沒想到半個月后汽烦,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡莉御,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年刹缝,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片颈将。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡梢夯,死狀恐怖,靈堂內的尸體忽然破棺而出晴圾,到底是詐尸還是另有隱情颂砸,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布死姚,位于F島的核電站人乓,受9級特大地震影響,放射性物質發(fā)生泄漏都毒。R本人自食惡果不足惜色罚,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望账劲。 院中可真熱鬧戳护,春花似錦金抡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至铺董,卻和暖如春巫击,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背精续。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工坝锰, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人重付。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓什黑,卻偏偏與公主長得像,于是被迫代替她去往敵國和親堪夭。 傳聞我的和親對象是個殘疾皇子愕把,可洞房花燭夜當晚...
    茶點故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內容