1. 程式人生 > >AFNetworking實現專案下載需求時遇過的那些坑

AFNetworking實現專案下載需求時遇過的那些坑


導語

當前市面上的APP,凡有涉及到視訊、期刊、或其它大型檔案傳輸、瀏覽等用途的,新增下載或快取至本地的功能以避免網速的限制及依賴,毫無疑問都將給使用者帶來更好的體驗。而談到下載技術,就又不得不牽扯到了斷點續傳,佇列任務等老生常談的問題。這不,本人當前的專案,就恰好遇到了這樣的需求。然而在經過大量調研之後,本人竟無法找到一篇總結得很好的文件,對此進行全面的介紹;能夠尋到的一些活躍度並不高的開源專案,卻又不能恰如其分並抱之以信賴滿足專案的需求。所以仔細斟酌後,不得不選擇自己動手,豐衣足食。鑽研的過程中遇到了不少坑、不少困難,有些個別的地方真是不用不知道,一用才知道是如此得蹩腳,難怪很少有人對此進行系統完整的介紹。現將本人在實現下載模組時所用到的技術總結如下,相信本文中所蘊涵的乾貨一定不會令費心閱讀的你感到失望!

話休絮煩。首先,說下載就離不開網路請求。而當今iOS開發技術當中,最廣泛使用的網路請求框架無疑要屬AFNetworking。經過對其進行簡單研究後,你就會尋到最適合用來完成下載這件“小事”的元件,叫做AFHTTPRequestOperation

現假定我們的需求是最常見,也是最能體現技術問題的一個,叫做:

下載佇列在某一時刻,最多僅能有一個下載任務處於正在下載的狀態中!

-- 敘述的節奏似乎稍稍快了些

那就先來看下實現佇列下載、斷點續傳等需求的關鍵示例程式碼吧!

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
NSError * error = nil; // 建立下載佇列 NSOperationQueue * downloadOperationQueue = [[NSOperationQueue alloc]init]; //  規定operationQueue中,最大可以同時執行的operation數量為1 downloadOperationQueue.maxConcurrentOperationCount = 1; // 建立單個下載任務(訪問已下載部分的檔案,實現斷點續傳) 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]; //  繼續某下載任務 [downloadOperation resume]; //  取消某下載任務(同時應將其已下載部分的檔案刪除) [downloadOperation cancel]; [[NSFileManager defaultManager] removeItemAtPath:DOWNLOADED_PART_FILE_PATH error:&error]; //  取消全部下載任務 [downloadOperationQueue cancelAllOperations]; //  此外還有若干方法用以判斷相應一見其名便知其義的狀態... downloadOperation.isReady downloadOperation.isExecuting downloadOperation.isPaused downloadOperation.isCancelled downloadOperation.isFinished //  判斷downloadOperation是否存在在downloadOperationQueue當中 [downloadOperationQueue.operations containsObject:downloadOperation]

以上程式碼建立了一個AFHTTPRequestOperation物件作為單個下載任務,並將其加入到NSOperationQueue中。仿照上例,我們可以建立多個AFHTTPRequestOperation物件並加入到NSOperationQueue中,即形成了下載佇列

做到這裡,你是不是認為已經沒有神馬技術問題啦?只要把operation一個個地新增到queue裡, 下載任務就可以一個接一個地自動執行了!而如果我們將上一個operation暫停、取消,或是它自然地下載完成了,又或是它下載中途失敗了,下一operation就會聰明地自動啟動,繼續其下載任務了!!?

錯!!!!

接下來筆者將要告訴你的,就是本文最最核心的乾貨,絕對顛覆你的想象!!

只要你親手動手試一試,就會發現如下大跌眼球的驚恐現象!!

驚人事實 1: 對queue中前一個下載operation執行pause方法,下一個operation並不能自動啟動進入正在執行的狀態!!

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

驚人事實 3: 注意到程式碼裡那個failure回撥的block了沒?它不僅僅將在operation執行失敗的時候被呼叫,還會在operation被cancel的時候被呼叫!!所以對於神馬叫做“operation的失敗”,你要重新建立起你的世界觀了!!

驚人事實 4: 如果對一個正處於pause狀態的operation執行cancel會怎麼樣?答案是這個operation還保留在queue中!!並且仍然保持著pause狀態!!僅有的一點變化,是它的isCancelled屬性,變成了YES!!

......未完待續,本文要令你感到驚詫的,還有很多

由於這些問題間相互關係的錯綜複雜,為了清晰條理地予以說明,特將本人實驗中所觀察到operation的行為總結如下表。

1.jpg

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

原因就在於AFHTTPRequestOperation的父類NSOperation,在設計之處就不是為了下載的操作而生的!人家開始就僅僅是用來處理多執行緒的啊!!所以造成了AFNetworking在擴充套件這個類的時候,可用的資源、介面等等就非常少。對於什麼下載任務暫停/繼續,下載中途失敗等等情況,很多問題幾乎就是沒有辦法理想地解決的,只好用NSOperation中僅有的幾種狀態予以並不貼切的表示。於是乎就出現了上表中種種詭異的情況

補充幾點乾貨。然後告訴你一個本文之前偷偷誤導了你的大坑!!

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

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

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

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

blob.png

頭痛、抓狂得很啊!!本人剛開始實現下載模組相關需求的時候,就被這些問題坑了個體無完膚。最後得出了本文最大的關鍵結論,也就是前面所說的“大坑”:

不能夠使用NSOperationQueue來進行多下載任務的管理!!!

理由如下:

  1. 你無法妥善地實現“佇列中最多僅能有一個下載任務正在進行”這條產品經理臆測會讓開發變簡單的需求!!比方說,你讓NSOperationQueue中一個operation暫停後,下一個任務並不會自動啟動啊!有人說可以手動去start下一個operation,如果這個姑且算做可以接受,可是問題又來了:我們沒有辦法手動將一個operation置為isReady狀態啊!!處於isReady狀態的operation,要麼是還未加入queue,要麼是加入了還未輪到執行,但是它只要一執行,就再也回不到isReady的狀態了!那我們要讓暫停的operation恢復到等待下載狀態該怎麼搞?此時可能還有另一operation正在執行啊!!反之筆者搞了半天,是無能為力了

  2. 下載是需要一定時間的過程,需要不停地向伺服器進行請求,那麼就永遠避免不了因為網路等原因中途會失敗的問題。可要命的是,一旦下載失敗,operation就會毫不妥協地從queue中被移除掉啊!!你能在這時候讓你的下載任務從UI介面上消失掉嗎?顯然大BOSS是不會允許你這麼幹的。有人說可以重建operation再加入到queue中,可那樣你只能將operation插到隊尾,列表順序就被打亂了啊!!你去瞧瞧看,operationQueue.operations,那可只是一個只讀屬性啊!!

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

不幸的事情來了。筆者最後只得放棄NSOperationQueue,使用古老原始的工具--NSMutableArray來進行多下載任務的管理。這樣的話所有operation的啟動、移除等操作都必須依靠手動來執行。這個辦法雖然辦法土了些,可是起碼對於每個operation的控制權又重新回到了我們手裡。有得必有失嘛!當能恰當地實現了專案需求的時候,這點犧牲也就算不上神馬了

在使用AFHTTPRequestOperation時我們還需要注意以下幾點:

  1. 對isReady狀態的operation執行resume、pause、cancel等方法是沒有任何用處的,所以為了確保執行正確,在對operation執行resume、pause、cancel前,都要首先執行[operation start]。(對已經start過的operation執行start不會造成任何影響)

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

  3. 對於下載模組這個糾結之處來說,本地持久化下載記錄的相關資料也是必不可少的,理由如下:

    1. AFHTTPRequestOperation、NSMutableArray這些都是執行時的東西,一關掉app,這些東西自然也都消失得無影無蹤了。我們能讓下載記錄就此消失得無影無蹤麼?NO!顯然是不能接受的 

    2. 我們下載得到的那個檔案,可能是已下載完成的,可能是隻下載了部分的;而只下載了部分這種的,又可能是下載中途暫停了的,失敗的,被取消的等等情況。請問單憑這個檔案如何判斷它是屬於哪種情況?而且這還不夠,有些下載任務根本可能就還未生成相應的下載檔案,app就已經被關了啊!你能就把這種的下載任務扔掉嗎?顯然是絕不可以的

    3. 不使用operationQueue我們同樣無法手動將operation標記為佇列等待的isReady狀態,怎麼辦?只有將operation設定為paused,然後相應的資料記錄標記為isReady狀態好了(本人使用的是CoreData進行本地持久化儲存)

    4. ......用operation外的資料模型記錄下載任務的狀態好處還有很多,但同時帶來的同步更新問題也有很多,具體就留給大家自己去體會了!

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