使用GCD實現和封裝分組併發網路請求

發表於2016-05-25

 

為什麼使用併發組請求?


在實際開發中我們通常會遇到這樣一種需求:某個頁面載入時通過網路請求獲得相應的資料,再做某些操作,有時候載入的內容需要通過好幾個請求的資料組合而成,比如有兩個請求A和B,我們通常為了省事,會將B請求放在A請求成功的回撥中發起,在B的成功回撥中將資料組合起來,這樣做有明顯的問題:

  1. 請求如果多了,需要寫許多巢狀的請求
  2. 如果在除了最後一個請求前的某個請求失敗了,就不會執行後面的請求,資料無法載入
  3. 請求變成同步的,這是最大的問題,在網路差的情況下,如果有n個請求,意味著使用者要等待n倍於併發請求的時間才能看到內容

dispatch_group併發組


熟悉dispatch_group的同學可以直接跳過這一節。

同步請求這麼low的方式當然是不可接受的,所以我們要併發這些請求,在所有請求都執行完成功回撥後,再做載入內容或其他操作,考慮再三,選擇用GCD的dispatch_group最方便。

A dispatch group is a mechanism for monitoring a set of blocks. Your application can monitor the blocks in the group synchronously or asynchronously depending on your needs. By extension, a group can be useful for synchronizing for code that depends on the completion of other tasks.

可以看出,dispatch_group專為監控block而生,並且蘋果也建議當你的某個操作依賴於其他幾個任務的完成時,可以使用dispatch_group。

dispatch_group通常有兩種用法:

  1. dispatch_group_async(, , )
    建立一個dispatch_group_t, 將併發的操作放在block中,在dispatch_group_notify(, , )的block中執行多組block執行完畢後的操作,對於網路請求來說,在請求發出時他就算執行完畢了,也就是block中還有個block的情況下,並不會等待網路請求的回撥,所以不滿足我們的需求。
    所以採用另一種用法:
  2. dispatch_group_enter()
    dispatch_group_leave()
    以下是dispatch_group_enter的官方文件解釋:

    Calling this function increments the current count of outstanding tasks in the group. Using this function (with dispatch_group_leave) allows your application to properly manage the task reference count if it explicitly adds and removes tasks from the group by a means other than using the dispatch_group_async function. A call to this function must be balanced with a call to dispatch_group_leave. You can use this function to associate a block with more than one group at the same time.

dispatch_group實際上有一個task reference count(任務計數器),enter時reference count +1,leave時reference count -1,enter和leave必須配合使用,有幾次enter就要有幾次leave,否則group會一直存在,dispatch_group_notify也不會觸發。

當所有enter的block都leave後,會執行dispatch_group_notify的block,這種方式顯然更加靈活。

我們當然可以在網路請求前enter,在執行完每個請求的成功或失敗回撥後leave,再在notify中執行內容載入。至此,併發網路組請求的問題就解決了,但還是有點小小不爽,每次發起組請求我都得建立group,寫一堆的enter和leave,既麻煩也不利於請求的複用,很自然我們想到把他封裝一下,最好能做到將一個網路請求加到組裡,而不用修改原先的網路請求程式碼,就像這樣:

組請求的封裝


如果我想做到這種效果,肯定要到網路單例層去對底層請求做些修改,但我又不想改變現有的底層請求方法,所以我採用了method_exchangeImplementations(, )這個函式,基於現有的底層請求方法,實現一套組的請求方法。在發組請求時,替換掉原先的方法,在組請求都傳送完畢後,再換回原先的方法。

但這裡有一些可怕的坑要處理,因為使用方法替換是很危險的。

  1. 我做了替換後,正常的非組網路請求也會走替換後的方法,但我不需要他走替換後的方法。
  2. 假如我同時發起了多個組請求,組和組之間要如何區分,不同的組是不應該相互影響的。

一開始我考慮給請求一個mark,標記他是屬於哪個group的,但這需要你已經把請求封裝成了一個物件,如果你的專案和我的一樣,發請求時只是執行一個方法,是不好給他加標記的。

在一陣頭腦風暴後,我決定用佇列來區分每個gorup。

具體做法就是建立group時,開啟一個佇列,給佇列動態新增group屬性,一個佇列對應一個group。在佇列中替換方法,發起組裡的請求,再替換回原先的方法。這樣在替換的方法裡只需要拿到當前的佇列,就可以拿到group,如果group是nil,說明是正常的非組請求,執行original method;如果group不是nil,根據group來enter和leave,這樣每個group也能區分開。

建立group時,給group動態新增一個errorArray屬性,用來記錄組裡請求的error,只要errorArray不為空,就會走組失敗的block。

附上完整程式碼:


單例中用來替換底層網路請求的組請求方法


提供給外界的組請求方法

經過這一番封裝,我在使用時,只需要在- (void)sendGroupPostRequest:(BlockAction)requests success:(BlockAction)success failure:(GroupResponseFailure)failure 這個方法的requests block中,把網路請求扔進去,原先寫好的請求不用做任何修改,請求本身的success和failure也都能執行,success block中寫組成功後要做的事情,比如內容載入,failure block中可以拿到每個請求的error,作相應處理。

相關文章