為什么使用并發(fā)組請(qǐng)求?
在實(shí)際開(kāi)發(fā)中我們通常會(huì)遇到這樣一種需求:某個(gè)頁(yè)面加載時(shí)通過(guò)網(wǎng)絡(luò)請(qǐng)求獲得相應(yīng)的數(shù)據(jù),再做某些操作,有時(shí)候加載的內(nèi)容需要通過(guò)好幾個(gè)請(qǐng)求的數(shù)據(jù)組合而成,比如有兩個(gè)請(qǐng)求A和B,我們通常為了省事,會(huì)將B請(qǐng)求放在A請(qǐng)求成功的回調(diào)中發(fā)起,在B的成功回調(diào)中將數(shù)據(jù)組合起來(lái),這樣做有明顯的問(wèn)題:
- 請(qǐng)求如果多了,需要寫(xiě)許多嵌套的請(qǐng)求
- 如果在除了最后一個(gè)請(qǐng)求前的某個(gè)請(qǐng)求失敗了,就不會(huì)執(zhí)行后面的請(qǐng)求,數(shù)據(jù)無(wú)法加載
- 請(qǐng)求變成同步的,這是最大的問(wèn)題,在網(wǎng)絡(luò)差的情況下,如果有n個(gè)請(qǐng)求,意味著用戶要等待n倍于并發(fā)請(qǐng)求的時(shí)間才能看到內(nèi)容
dispatch_group并發(fā)組
熟悉dispatch_group的同學(xué)可以直接跳過(guò)這一節(jié)。
同步請(qǐng)求這么low的方式當(dāng)然是不可接受的,所以我們要并發(fā)這些請(qǐng)求,在所有請(qǐng)求都執(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專(zhuān)為監(jiān)控block而生,并且蘋(píng)果也建議當(dāng)你的某個(gè)操作依賴(lài)于其他幾個(gè)任務(wù)的完成時(shí),可以使用dispatch_group。
dispatch_group通常有兩種用法:
dispatch_group_async(<#dispatch_group_t group#>, <#dispatch_queue_t queue#>, <#^(void)block#>)
創(chuàng)建一個(gè)dispatch_group_t, 將并發(fā)的操作放在block中,在dispatch_group_notify(<#dispatch_group_t group#>, <#dispatch_queue_t queue#>, <#^(void)block#>)的block中執(zhí)行多組block執(zhí)行完畢后的操作,對(duì)于網(wǎng)絡(luò)請(qǐng)求來(lái)說(shuō),在請(qǐng)求發(fā)出時(shí)他就算執(zhí)行完畢了,也就是block中還有個(gè)block的情況下,并不會(huì)等待網(wǎng)絡(luò)請(qǐng)求的回調(diào),所以不滿足我們的需求。
所以采用另一種用法:dispatch_group_enter(<#dispatch_group_t group#>)
dispatch_group_leave(<#dispatch_group_t group#>)
以下是dispatch_group_enter的官方文檔解釋?zhuān)?/p>
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實(shí)際上有一個(gè)task reference count(任務(wù)計(jì)數(shù)器),enter時(shí)reference count +1,leave時(shí)reference count -1,enter和leave必須配合使用,有幾次enter就要有幾次leave,否則group會(huì)一直存在,dispatch_group_notify也不會(huì)觸發(fā)。
當(dāng)所有enter的block都leave后,會(huì)執(zhí)行dispatch_group_notify的block,這種方式顯然更加靈活。
我們當(dāng)然可以在網(wǎng)絡(luò)請(qǐng)求前enter,在執(zhí)行完每個(gè)請(qǐng)求的成功或失敗回調(diào)后leave,再在notify中執(zhí)行內(nèi)容加載。至此,并發(fā)網(wǎng)絡(luò)組請(qǐng)求的問(wèn)題就解決了,但還是有點(diǎn)小小不爽,每次發(fā)起組請(qǐng)求我都得創(chuàng)建group,寫(xiě)一堆的enter和leave,既麻煩也不利于請(qǐng)求的復(fù)用,很自然我們想到把他封裝一下,最好能做到將一個(gè)網(wǎng)絡(luò)請(qǐng)求加到組里,而不用修改原先的網(wǎng)絡(luò)請(qǐng)求代碼,就像這樣:
[[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
}];
組請(qǐng)求的封裝
如果我想做到這種效果,肯定要到網(wǎng)絡(luò)單例層去對(duì)底層請(qǐng)求做些修改,但我又不想改變現(xiàn)有的底層請(qǐng)求方法,所以我采用了method_exchangeImplementations(<#Method m1#>, <#Method m2#>)這個(gè)函數(shù),基于現(xiàn)有的底層請(qǐng)求方法,實(shí)現(xiàn)一套組的請(qǐng)求方法。在發(fā)組請(qǐng)求時(shí),替換掉原先的方法,在組請(qǐng)求都發(fā)送完畢后,再換回原先的方法。
但這里有一些可怕的坑要處理,因?yàn)槭褂梅椒ㄌ鎿Q是很危險(xiǎn)的。
- 我做了替換后,正常的非組網(wǎng)絡(luò)請(qǐng)求也會(huì)走替換后的方法,但我不需要他走替換后的方法。
- 假如我同時(shí)發(fā)起了多個(gè)組請(qǐng)求,組和組之間要如何區(qū)分,不同的組是不應(yīng)該相互影響的。
一開(kāi)始我考慮給請(qǐng)求一個(gè)mark,標(biāo)記他是屬于哪個(gè)group的,但這需要你已經(jīng)把請(qǐng)求封裝成了一個(gè)對(duì)象,如果你的項(xiàng)目和我的一樣,發(fā)請(qǐng)求時(shí)只是執(zhí)行一個(gè)方法,是不好給他加標(biāo)記的。
在一陣頭腦風(fēng)暴后,我決定用隊(duì)列來(lái)區(qū)分每個(gè)gorup。
具體做法就是創(chuàng)建group時(shí),開(kāi)啟一個(gè)隊(duì)列,給隊(duì)列動(dòng)態(tài)添加group屬性,一個(gè)隊(duì)列對(duì)應(yīng)一個(gè)group。在隊(duì)列中替換方法,發(fā)起組里的請(qǐng)求,再替換回原先的方法。這樣在替換的方法里只需要拿到當(dāng)前的隊(duì)列,就可以拿到group,如果group是nil,說(shuō)明是正常的非組請(qǐng)求,執(zhí)行original method;如果group不是nil,根據(jù)group來(lái)enter和leave,這樣每個(gè)group也能區(qū)分開(kāi)。
創(chuàng)建group時(shí),給group動(dòng)態(tài)添加一個(gè)errorArray屬性,用來(lái)記錄組里請(qǐng)求的error,只要errorArray不為空,就會(huì)走組失敗的block。
附上完整代碼:
typedef void(^BlockAction)();
typedef void(^GroupResponseFailure)(NSArray * errorArray);
static char groupErrorKey;
static char queueGroupKey;
單例中用來(lái)替換底層網(wǎng)絡(luò)請(qǐng)求的組請(qǐng)求方法
- (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);
// 如果是非組請(qǐng)求
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);
}];
}
提供給外界的組請(qǐng)求方法
- (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ā)起請(qǐng)求會(huì)調(diào)用上面的組請(qǐng)求方法
requests();
// 發(fā)出請(qǐng)求后就可以替換回original method,不必等待回調(diào),盡量減小替換的時(shí)間窗口
method_exchangeImplementations(originalPost, groupPost);
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSMutableArray *arrayM = objc_getAssociatedObject(group, &groupErrorKey);
// 只要組里的一個(gè)請(qǐng)求失敗,就走組失敗的回調(diào)
if (arrayM.count > 0) {
if (failure) {
failure(arrayM.copy);
}
} else if(success) {
success();
}
});
}];
}
經(jīng)過(guò)這一番封裝,我在使用時(shí),只需要在- (void)sendGroupPostRequest:(BlockAction)requests success:(BlockAction)success failure:(GroupResponseFailure)failure 這個(gè)方法的requests block中,把網(wǎng)絡(luò)請(qǐng)求扔進(jìn)去,原先寫(xiě)好的請(qǐng)求不用做任何修改,請(qǐng)求本身的success和failure也都能執(zhí)行,success block中寫(xiě)組成功后要做的事情,比如內(nèi)容加載,failure block中可以拿到每個(gè)請(qǐng)求的error,作相應(yīng)處理。