為什么使用并發(fā)組請求沐悦?
在實際開發(fā)中我們通常會遇到這樣一種需求:某個頁面加載時通過網(wǎng)絡(luò)請求獲得相應(yīng)的數(shù)據(jù)涎显,再做某些操作摧茴,有時候加載的內(nèi)容需要通過好幾個請求的數(shù)據(jù)組合而成哄褒,比如有兩個請求A和B稀蟋,我們通常為了省事,會將B請求放在A請求成功的回調(diào)中發(fā)起呐赡,在B的成功回調(diào)中將數(shù)據(jù)組合起來退客,這樣做有明顯的問題:
- 請求如果多了,需要寫許多嵌套的請求
- 如果在除了最后一個請求前的某個請求失敗了链嘀,就不會執(zhí)行后面的請求萌狂,數(shù)據(jù)無法加載
- 請求變成同步的,這是最大的問題怀泊,在網(wǎng)絡(luò)差的情況下茫藏,如果有n個請求,意味著用戶要等待n倍于并發(fā)請求的時間才能看到內(nèi)容
dispatch_group并發(fā)組
熟悉dispatch_group的同學(xué)可以直接跳過這一節(jié)包个。
同步請求這么low的方式當(dāng)然是不可接受的刷允,所以我們要并發(fā)這些請求冤留,在所有請求都執(zhí)行完成功回調(diào)后碧囊,再做加載內(nèi)容或其他操作,考慮再三纤怒,選擇用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專為監(jiān)控block而生泊窘,并且蘋果也建議當(dāng)你的某個操作依賴于其他幾個任務(wù)的完成時熄驼,可以使用dispatch_group像寒。
dispatch_group通常有兩種用法:
dispatch_group_async(<#dispatch_group_t group#>, <#dispatch_queue_t queue#>, <#^(void)block#>)
創(chuàng)建一個dispatch_group_t, 將并發(fā)的操作放在block中瓜贾,在dispatch_group_notify(<#dispatch_group_t group#>, <#dispatch_queue_t queue#>, <#^(void)block#>)
的block中執(zhí)行多組block執(zhí)行完畢后的操作诺祸,對于網(wǎng)絡(luò)請求來說,在請求發(fā)出時他就算執(zhí)行完畢了祭芦,也就是block中還有個block的情況下筷笨,并不會等待網(wǎng)絡(luò)請求的回調(diào),所以不滿足我們的需求龟劲。
所以采用另一種用法:dispatch_group_enter(<#dispatch_group_t group#>)
dispatch_group_leave(<#dispatch_group_t group#>)
以下是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(任務(wù)計數(shù)器)胃夏,enter時reference count +1,leave時reference count -1昌跌,enter和leave必須配合使用仰禀,有幾次enter就要有幾次leave,否則group會一直存在蚕愤,dispatch_group_notify也不會觸發(fā)答恶。
當(dāng)所有enter的block都leave后,會執(zhí)行dispatch_group_notify的block萍诱,這種方式顯然更加靈活亥宿。
我們當(dāng)然可以在網(wǎng)絡(luò)請求前enter,在執(zhí)行完每個請求的成功或失敗回調(diào)后leave砂沛,再在notify中執(zhí)行內(nèi)容加載烫扼。至此,并發(fā)網(wǎng)絡(luò)組請求的問題就解決了碍庵,但還是有點小小不爽映企,每次發(fā)起組請求我都得創(chuàng)建group,寫一堆的enter和leave静浴,既麻煩也不利于請求的復(fù)用堰氓,很自然我們想到把他封裝一下,最好能做到將一個網(wǎng)絡(luò)請求加到組里苹享,而不用修改原先的網(wǎng)絡(luò)請求代碼双絮,就像這樣:
[[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
}];
組請求的封裝
如果我想做到這種效果,肯定要到網(wǎng)絡(luò)單例層去對底層請求做些修改得问,但我又不想改變現(xiàn)有的底層請求方法囤攀,所以我采用了method_exchangeImplementations(<#Method m1#>, <#Method m2#>)
這個函數(shù),基于現(xiàn)有的底層請求方法宫纬,實現(xiàn)一套組的請求方法焚挠。在發(fā)組請求時,替換掉原先的方法漓骚,在組請求都發(fā)送完畢后蝌衔,再換回原先的方法榛泛。
但這里有一些可怕的坑要處理,因為使用方法替換是很危險的噩斟。
- 我做了替換后曹锨,正常的非組網(wǎng)絡(luò)請求也會走替換后的方法,但我不需要他走替換后的方法剃允。
- 假如我同時發(fā)起了多個組請求艘希,組和組之間要如何區(qū)分,不同的組是不應(yīng)該相互影響的硅急。
一開始我考慮給請求一個mark覆享,標(biāo)記他是屬于哪個group的,但這需要你已經(jīng)把請求封裝成了一個對象营袜,如果你的項目和我的一樣撒顿,發(fā)請求時只是執(zhí)行一個方法,是不好給他加標(biāo)記的荚板。
在一陣頭腦風(fēng)暴后凤壁,我決定用隊列來區(qū)分每個gorup。
具體做法就是創(chuàng)建group時跪另,開啟一個隊列拧抖,給隊列動態(tài)添加group屬性,一個隊列對應(yīng)一個group免绿。在隊列中替換方法唧席,發(fā)起組里的請求,再替換回原先的方法嘲驾。這樣在替換的方法里只需要拿到當(dāng)前的隊列淌哟,就可以拿到group,如果group是nil辽故,說明是正常的非組請求徒仓,執(zhí)行original method;如果group不是nil誊垢,根據(jù)group來enter和leave掉弛,這樣每個group也能區(qū)分開。
創(chuàng)建group時喂走,給group動態(tài)添加一個errorArray屬性殃饿,用來記錄組里請求的error,只要errorArray不為空缴啡,就會走組失敗的block壁晒。
附上完整代碼:
typedef void(^BlockAction)();
typedef void(^GroupResponseFailure)(NSArray * errorArray);
static char groupErrorKey;
static char queueGroupKey;
單例中用來替換底層網(wǎng)絡(luò)請求的組請求方法
- (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) {
// 執(zhí)行original method
[self sendPOSTRequestInGroup:strURL withData:data paramForm:paramForm withTimeout:timeout showAlert:show success:success failure:failure];
return;
}
dispatch_group_enter(group);
// 執(zhí)行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);
}];
}
提供給外界的組請求方法
- (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);
// 現(xiàn)在發(fā)起請求會調(diào)用上面的組請求方法
requests();
// 發(fā)出請求后就可以替換回original method瓷们,不必等待回調(diào)业栅,盡量減小替換的時間窗口
method_exchangeImplementations(originalPost, groupPost);
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSMutableArray *arrayM = objc_getAssociatedObject(group, &groupErrorKey);
// 只要組里的一個請求失敗秒咐,就走組失敗的回調(diào)
if (arrayM.count > 0) {
if (failure) {
failure(arrayM.copy);
}
} else if(success) {
success();
}
});
}];
}
經(jīng)過這一番封裝,我在使用時碘裕,只需要在- (void)sendGroupPostRequest:(BlockAction)requests success:(BlockAction)success failure:(GroupResponseFailure)failure
這個方法的requests block中携取,把網(wǎng)絡(luò)請求扔進去,原先寫好的請求不用做任何修改帮孔,請求本身的success和failure也都能執(zhí)行雷滋,success block中寫組成功后要做的事情,比如內(nèi)容加載文兢,failure block中可以拿到每個請求的error晤斩,作相應(yīng)處理。