為什麼使用併發組請求?
在實際開發中我們通常會遇到這樣一種需求:某個頁面載入時通過網路請求獲得相應的資料,再做某些操作,有時候載入的內容需要通過好幾個請求的資料組合而成,比如有兩個請求A和B,我們通常為了省事,會將B請求放在A請求成功的回撥中發起,在B的成功回撥中將資料組合起來,這樣做有明顯的問題:
- 請求如果多了,需要寫許多巢狀的請求
- 如果在除了最後一個請求前的某個請求失敗了,就不會執行後面的請求,資料無法載入
- 請求變成同步的,這是最大的問題,在網路差的情況下,如果有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通常有兩種用法:
dispatch_group_async(, , )
建立一個dispatch_group_t, 將併發的操作放在block中,在dispatch_group_notify(, , )
的block中執行多組block執行完畢後的操作,對於網路請求來說,在請求發出時他就算執行完畢了,也就是block中還有個block的情況下,並不會等待網路請求的回撥,所以不滿足我們的需求。
所以採用另一種用法: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,既麻煩也不利於請求的複用,很自然我們想到把他封裝一下,最好能做到將一個網路請求加到組裡,而不用修改原先的網路請求程式碼,就像這樣:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
[[NetworkTool sharedInstance] postForGroup:^{ [request1 success:^(id responseObject) { } failure:^(NSError *error) { }]; [request2 success:^(id responseObject) { } failure:^(NSError *error) { }]; } success:^{ // group success } failure:^(NSArray *errorArray) { // group failure }]; |
組請求的封裝
如果我想做到這種效果,肯定要到網路單例層去對底層請求做些修改,但我又不想改變現有的底層請求方法,所以我採用了method_exchangeImplementations(, )
這個函式,基於現有的底層請求方法,實現一套組的請求方法。在發組請求時,替換掉原先的方法,在組請求都傳送完畢後,再換回原先的方法。
但這裡有一些可怕的坑要處理,因為使用方法替換是很危險的。
- 我做了替換後,正常的非組網路請求也會走替換後的方法,但我不需要他走替換後的方法。
- 假如我同時發起了多個組請求,組和組之間要如何區分,不同的組是不應該相互影響的。
一開始我考慮給請求一個mark,標記他是屬於哪個group的,但這需要你已經把請求封裝成了一個物件,如果你的專案和我的一樣,發請求時只是執行一個方法,是不好給他加標記的。
在一陣頭腦風暴後,我決定用佇列來區分每個gorup。
具體做法就是建立group時,開啟一個佇列,給佇列動態新增group屬性,一個佇列對應一個group。在佇列中替換方法,發起組裡的請求,再替換回原先的方法。這樣在替換的方法裡只需要拿到當前的佇列,就可以拿到group,如果group是nil,說明是正常的非組請求,執行original method;如果group不是nil,根據group來enter和leave,這樣每個group也能區分開。
建立group時,給group動態新增一個errorArray屬性,用來記錄組裡請求的error,只要errorArray不為空,就會走組失敗的block。
附上完整程式碼:
1 2 3 4 |
typedef void(^BlockAction)(); typedef void(^GroupResponseFailure)(NSArray * errorArray); static char groupErrorKey; static char queueGroupKey; |
單例中用來替換底層網路請求的組請求方法
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 |
- (void)sendPOSTRequestInGroup:(NSString *)strURL withData:(NSDictionary *)data paramForm:(ParamForm)paramForm withTimeout:(NSTimeInterval)timeout showAlert:(BOOL)show success:(BlockResponse)success failure:(BlockResponseFailure)failure { dispatch_group_t group = objc_getAssociatedObject([NSOperationQueue currentQueue], &queueGroupKey); // 如果是非組請求 if (group == nil) { // 執行original method [self sendPOSTRequestInGroup:strURL withData:data paramForm:paramForm withTimeout:timeout showAlert:show success:success failure:failure]; return; } dispatch_group_enter(group); // 執行original method [self sendPOSTRequestInGroup:strURL withData:data paramForm:paramForm withTimeout:timeout showAlert:show success:^(id responseObject) { if (success) { success(responseObject); } dispatch_group_leave(group); } failure:^(NSError *error) { NSMutableArray *arrayM = objc_getAssociatedObject(group, &groupErrorKey); [arrayM addObject:error]; if (failure) { failure(error); } dispatch_group_leave(group); }]; } |
提供給外界的組請求方法
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 |
- (void)sendGroupPostRequest:(BlockAction)requests success:(BlockAction)success failure:(GroupResponseFailure)failure { if (requests == nil) { return; } dispatch_group_t group = dispatch_group_create(); objc_setAssociatedObject(group, &groupErrorKey, [NSMutableArray array], OBJC_ASSOCIATION_RETAIN_NONATOMIC); Method originalPost = class_getInstanceMethod(self.class, @selector(sendPOSTRequest:withData:paramForm:withTimeout:showAlert:success:failure:)); Method groupPost = class_getInstanceMethod(self.class, @selector(sendPOSTRequestInGroup:withData:paramForm:withTimeout:showAlert:success:failure:)); NSOperationQueue *queue = [[NSOperationQueue alloc] init]; objc_setAssociatedObject(queue, &queueGroupKey, group, OBJC_ASSOCIATION_RETAIN_NONATOMIC); queue.qualityOfService = NSQualityOfServiceUserInitiated; queue.maxConcurrentOperationCount = 3; [queue addOperationWithBlock:^{ method_exchangeImplementations(originalPost, groupPost); // 現在發起請求會呼叫上面的組請求方法 requests(); // 發出請求後就可以替換回original method,不必等待回撥,儘量減小替換的時間視窗 method_exchangeImplementations(originalPost, groupPost); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSMutableArray *arrayM = objc_getAssociatedObject(group, &groupErrorKey); // 只要組裡的一個請求失敗,就走組失敗的回撥 if (arrayM.count > 0) { if (failure) { failure(arrayM.copy); } } else if(success) { success(); } }); }]; } |
經過這一番封裝,我在使用時,只需要在- (void)sendGroupPostRequest:(BlockAction)requests success:(BlockAction)success failure:(GroupResponseFailure)failure
這個方法的requests block中,把網路請求扔進去,原先寫好的請求不用做任何修改,請求本身的success和failure也都能執行,success block中寫組成功後要做的事情,比如內容載入,failure block中可以拿到每個請求的error,作相應處理。