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

  • 當前市面上的APP,凡有涉及到視頻、期刊、或其它大型文件傳輸、瀏覽等用途的,添加下載或緩存至本地的功能以避免網(wǎng)速的限制及依賴,毫無疑問都將給用戶帶來更好的體驗。而談到下載技術(shù),就又不得不牽扯到了斷點續(xù)傳,隊列任務(wù)等老生常談的問題。然而在經(jīng)過大量調(diào)研之后,本人竟無法找到一篇總結(jié)得很好的文檔,對此進行全面的介紹;能夠?qū)さ降囊恍┗钴S度并不高的開源項目,卻又不能恰如其分并抱之以信賴滿足項目的需求。所以仔細斟酌后,不得不選擇自己動手,豐衣足食。鉆研的過程中遇到了不少坑、不少困難,有些個別的地方真是不用不知道,一用才知道是如此得蹩腳,難怪很少有人對此進行系統(tǒng)完整的介紹。現(xiàn)將本人在實現(xiàn)下載模塊時所用到的技術(shù)總結(jié)如下,相信本文中所蘊涵的干貨一定不會令費心閱讀的你感到失望!

  • 話休絮煩。首先,說下載就離不開網(wǎng)絡(luò)請求。而當今iOS開發(fā)技術(shù)當中,最廣泛使用的網(wǎng)絡(luò)請求框架無疑要屬AFNetworking。經(jīng)過對其進行簡單研究后,你就會尋到最適合用來完成下載這件“小事”的組件,叫做AFHTTPRequestOperation

現(xiàn)假定我們的需求是最常見,也是最能體現(xiàn)技術(shù)問題的一個,叫做:
  • 下載隊列在某一時刻,最多僅能有一個下載任務(wù)處于正在下載的狀態(tài)中!
我們先來看下實現(xiàn)隊列下載、斷點續(xù)傳等需求的關(guān)鍵示例代碼:
    NSError * error = nil;
    
    // 創(chuàng)建下載隊列
    NSOperationQueue * downloadOperationQueue = [[NSOperationQueue alloc]init];
    //  規(guī)定operationQueue中,最大可以同時執(zhí)行的operation數(shù)量為1
    downloadOperationQueue.maxConcurrentOperationCount = 1;
    
    // 創(chuàng)建單個下載任務(wù)(訪問已下載部分的文件,實現(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");
    }];
    
    //  將單個下載任務(wù)加入到下載隊列當中
    [downloadOperationQueue addOperation:downloadOperation];
    
    //  暫停某下載任務(wù)
    [downloadOperation pause];
    
    //  繼續(xù)某下載任務(wù)
    [downloadOperation resume];
    
    //  取消某下載任務(wù)(同時應(yīng)將其已下載部分的文件刪除)
    [downloadOperation cancel];
    [[NSFileManager defaultManager] removeItemAtPath:DOWNLOADED_PART_FILE_PATH error:&error];
    
    //  取消全部下載任務(wù)
    [downloadOperationQueue cancelAllOperations];
    
    //  此外還有若干方法用以判斷相應(yīng)一見其名便知其義的狀態(tài)...
    downloadOperation.isReady
    downloadOperation.isExecuting
    downloadOperation.isPaused
    downloadOperation.isCancelled
    downloadOperation.isFinished
    
    //  判斷downloadOperation是否存在在downloadOperationQueue當中
    [downloadOperationQueue.operations containsObject:downloadOperation]
  • 以上代碼創(chuàng)建了一個AFHTTPRequestOperation對象作為單個下載任務(wù),并將其加入到NSOperationQueue中。仿照上例,我們可以創(chuàng)建多個AFHTTPRequestOperation對象并加入到NSOperationQueue中,即形成了下載隊列

  • 做到這里,你是不是認為已經(jīng)沒有神馬技術(shù)問題啦?只要把operation一個個地添加到queue里, 下載任務(wù)就可以一個接一個地自動執(zhí)行了!而如果我們將上一個operation暫停、取消,或是它自然地下載完成了,又或是它下載中途失敗了,下一operation就會聰明地自動啟動,繼續(xù)其下載任務(wù)了?。??

錯?。?!

  • 接下來筆者將要告訴你的,就是本文最最核心的干貨
    ,絕對顛覆你的想象?。?/p>

  • 只要你親手動手試一試,就會發(fā)現(xiàn)如下大跌眼球的驚恐現(xiàn)象?。?/p>

驚人事實 1: 對queue中前一個下載operation執(zhí)行pause方法,下一個operation并不能自動啟動進入正在執(zhí)行的狀態(tài)?。?/p>

驚人事實 2: 如果queue中前一個下載operation執(zhí)行失敗了(可用下載中途斷網(wǎng)進行模擬),它將從queue中自動地被移除掉??!

驚人事實 3: 注意到代碼里那個failure回調(diào)的block了沒?它不僅僅將在operation執(zhí)行失敗的時候被調(diào)用,還會在operation被cancel的時候被調(diào)用?。∷詫τ谏耨R叫做“operation的失敗”,你要重新建立起你的世界觀了?。?/p>

驚人事實 4: 如果對一個正處于pause狀態(tài)的operation執(zhí)行cancel會怎么樣?答案是這個operation還保留在queue中??!并且仍然保持著pause狀態(tài)??!僅有的一點變化,是它的isCancelled屬性,變成了YES?。?/p>

  • 有木有感到AFHTTPRequestOperationNSOperationQueue是個多么坑爹的東東?為何就不能像我們想象中一樣用得舒爽?

  • 原因就在于AFHTTPRequestOperation的父類NSOperation,在設(shè)計之處就不是為了下載的操作而生的!人家開始就僅僅是用來處理多線程的?。?!所以造成了AFNetworking在擴展這個類的時候,可用的資源、接口等等就非常少。對于什么下載任務(wù)暫停/繼續(xù),下載中途失敗等等情況,很多問題幾乎就是沒有辦法理想地解決的,只好用NSOperation中僅有的幾種狀態(tài)予以并不貼切的表示。于是乎就出現(xiàn)了上表中種種詭異的情況

  • 補充幾點干貨。然后告訴你一個本文之前偷偷誤導(dǎo)了你的大坑??!

驚人事實 5:如果一個queue中有一個下載operation正在執(zhí)行,此時對另一處在isReady狀態(tài)的operation執(zhí)行start方法會怎么樣?你很可能會說:“沒用的,因為之前設(shè)了queue.maxConcurrentOperationCount = 1嘛!” 可事實恰好相反,這個operation也會立刻被啟動執(zhí)行??!于是乎你不忍心看到的事情就出現(xiàn)了,這時queue將會有兩個任務(wù)被同時執(zhí)行??!maxConcurrentOperationCount完全失效了!!

驚人事實 6:承接上一點,如果此時另一條的狀態(tài)不是isReady,而是isPaused暫停狀態(tài),你對其執(zhí)行resume方法,此時會怎么樣呢?哈哈,沒錯,你吸取了上一條的經(jīng)驗,終于猜對了!這個operation也會立刻啟動被執(zhí)行,不管當前的queue有沒有另一個operation正在被執(zhí)行!!從中我們就可以意識到,maxConcurrentOperationCount這個屬性,只能管得自動啟動每一operation時,先檢查下是否正在執(zhí)行的operation的數(shù)量已經(jīng)超過那個數(shù)字了;可是如果你要手動start某一operation,對不起,這條限制半點都沒有用處了......

驚人事實 7:從上表中我們可以看到,無論是一個operation自然地執(zhí)行完畢,還是中途失敗,還是被執(zhí)行了cancel方法,都會被標記為isFinished,從operation中被移除掉,operation所認為的“完成”可完全不像我們想象中的那么狹義!問題來了,此時如果再對這個operation執(zhí)行start方法會怎么樣?對不起!沒有任何用處!??
所以你如果想要讓一個已失敗的operation從斷點處繼續(xù)再開始執(zhí)行下載該怎么辦?不好意思,只好新建operation重新再來了......

  • 基于實驗我們又可以得出了這樣的一張流程圖)基于實驗我們又可以得出了這樣的一張流程圖:

  • 本人剛開始實現(xiàn)下載模塊相關(guān)需求的時候,就被這些問題坑了個體無完膚。最后得出了本文最大的關(guān)鍵結(jié)論,也就是前面所說的“大坑”:

不能夠使用NSOperationQueue來進行多下載任務(wù)的管理?。。?/h5>

理由如下:

  • 你無法妥善地實現(xiàn)“隊列中最多僅能有一個下載任務(wù)正在進行”這條產(chǎn)品經(jīng)理臆測會讓開發(fā)變簡單的需求??!比方說,你讓NSOperationQueue中一個operation暫停后,下一個任務(wù)并不會自動啟動啊!有人說可以手動去start下一個operation,如果這個姑且算做可以接受,可是問題又來了:我們沒有辦法手動將一個operation置為isReady狀態(tài)?。?!處于isReady狀態(tài)的operation,要么是還未加入queue,要么是加入了還未輪到執(zhí)行,但是它只要一執(zhí)行,就再也回不到isReady的狀態(tài)了!那我們要讓暫停的operation恢復(fù)到等待下載狀態(tài)該怎么搞?此時可能還有另一operation正在執(zhí)行?。。》粗P者搞了半天,是無能為力了
  • 下載是需要一定時間的過程,需要不停地向服務(wù)器進行請求,那么就永遠避免不了因為網(wǎng)絡(luò)等原因中途會失敗的問題。可要命的是,一旦下載失敗,operation就會毫不妥協(xié)地從queue中被移除掉?。?!你能在這時候讓你的下載任務(wù)從UI界面上消失掉嗎?顯然大BOSS是不會允許你這么干的。有人說可以重建operation再加入到queue中,可那樣你只能將operation插到隊尾,列表順序就被打亂了?。。∧闳デ魄瓶?,operationQueue.operations,那可只是一個只讀屬性?。。?/li>

......自己去體會吧,反正坑多的已經(jīng)無力吐槽,再堅持下去也是枉費心思了。

  • 不幸的事情來了。筆者最后只得放棄NSOperationQueue,使用古老原始的工具--NSMutableArray來進行多下載任務(wù)的管理。這樣的話所有operation的啟動、移除等操作都必須依靠手動來執(zhí)行。這個辦法雖然辦法土了些,可是起碼對于每個operation的控制權(quán)又重新回到了我們手里。有得必有失嘛!當能恰當?shù)貙崿F(xiàn)了項目需求的時候,這點犧牲也就算不上神馬了
在使用AFHTTPRequestOperation時我們還需要注意以下幾點:
  • 對isReady狀態(tài)的operation執(zhí)行resume、pause、cancel等方法是沒有任何用處的,所以為了確保執(zhí)行正確,在對operation執(zhí)行resume、pause、cancel前,都要首先執(zhí)行[operation start]。(對已經(jīng)start過的operation執(zhí)行start不會造成任何影響)

  • 對處于isPaused的operation執(zhí)行cancel方法是無法得到正確結(jié)果的,所以每次執(zhí)行cancel方法前,都要先執(zhí)行一下[operation resume]。 (同樣對于正處于isExecuting狀態(tài)的operation來說,執(zhí)行resume方法也是不會造成任何影響的)

  • 對于下載模塊這個糾結(jié)之處來說,本地持久化下載記錄的相關(guān)數(shù)據(jù)也是必不可少的,理由如下:
    a. AFHTTPRequestOperation、NSMutableArray這些都是運行時的東西,一關(guān)掉app,這些東西自然也都消失得無影無蹤了。我們能讓下載記錄就此消失得無影無蹤么?NO!顯然是不能接受的
    b. 我們下載得到的那個文件,可能是已下載完成的,可能是只下載了部分的;而只下載了部分這種的,又可能是下載中途暫停了的,失敗的,被取消的等等情況。請問單憑這個文件如何判斷它是屬于哪種情況?而且這還不夠,有些下載任務(wù)根本可能就還未生成相應(yīng)的下載文件,app就已經(jīng)被關(guān)了??!你能就把這種的下載任務(wù)扔掉嗎?顯然是絕不可以的
    c. 不使用operationQueue我們同樣無法手動將operation標記為隊列等待的isReady狀態(tài),怎么辦?只有將operation設(shè)定為paused,然后相應(yīng)的數(shù)據(jù)記錄標記為isReady狀態(tài)好了(本人使用的是CoreData進行本地持久化存儲)
    d. ......用operation外的數(shù)據(jù)模型記錄下載任務(wù)的狀態(tài)好處還有很多,但同時帶來的同步更新問題也有很多,具體就留給大家自己去體會了!

  • 以上就是本人總結(jié)下載模塊實現(xiàn)時需要注意到的種種內(nèi)容。當然各位大神如果有更好的方案提出,比如用本人掌握得還不夠好的stream如何實現(xiàn)上述需求,本人也愿虛心聽取以將此處完善得更好。歡迎直言批評與不吝賜教!

轉(zhuǎn)載請注明出處有夢想的老伯伯

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內(nèi)容