- 存在的問題
最近在做項(xiàng)目的過程中,發(fā)生APP啟動(dòng)的時(shí)候,有些接口會(huì)在短時(shí)間(例如1s)之內(nèi)重復(fù)調(diào)用多次,在APP執(zhí)行某些操作時(shí),也時(shí)不時(shí)出現(xiàn)一個(gè)接口短時(shí)間內(nèi)調(diào)用多次的情況。究其原因,有些請(qǐng)求重復(fù)是因?yàn)樵诓煌臅r(shí)機(jī)點(diǎn)都需要請(qǐng)求這個(gè)接口,但是有時(shí)候這些不同時(shí)機(jī)點(diǎn),在短時(shí)間內(nèi)會(huì)一起發(fā)生,例如如下這種情況:
- (void)applicationDidBecomeActive:(UIApplication *)application {
// 請(qǐng)求接口
}
// 首頁控制器
- (void)viewDidLoad {
// 請(qǐng)求接口
}
我需要在app回到前臺(tái)時(shí)請(qǐng)求接口,在首頁控制器加載完后也需要請(qǐng)求接口,這兩個(gè)不同的時(shí)機(jī),會(huì)在APP啟動(dòng)時(shí)短時(shí)間內(nèi)都發(fā)生,導(dǎo)致接口重復(fù)請(qǐng)求了。雖然可以針對(duì)這種情況寫一些判斷代碼,當(dāng)每發(fā)生類似情況都寫一次判斷就不優(yōu)雅了。
還有些重復(fù)請(qǐng)求的情況是代碼一開始設(shè)計(jì)沒有考慮那么完善,后面代碼迭代,就會(huì)導(dǎo)致有些邏輯重復(fù)走幾次,接口也重復(fù)請(qǐng)求了。這種改起來就要小心翼翼了,一不小心就容易改出bug。
- 解決問題的思路
所以仔細(xì)思考后,還是決定統(tǒng)一處理,從請(qǐng)求的底層下手,在請(qǐng)求的底層做重復(fù)請(qǐng)求的管理,提供一個(gè)過濾方法給業(yè)務(wù)層,業(yè)務(wù)層只需要把原來的請(qǐng)求接口的方法替換成有過濾功能的方法就可以了,不需要改動(dòng)業(yè)務(wù)邏輯。
那么應(yīng)該如何設(shè)計(jì)這個(gè)對(duì)外提供的方法呢,我這里設(shè)計(jì)為方法會(huì)提供一個(gè)參數(shù),可以設(shè)置過濾時(shí)間,在過濾時(shí)間范圍內(nèi),相同的請(qǐng)求只會(huì)發(fā)出去一次:
@interface EPRequest : YTKRequest<NSCoding>
// url
@property (nonatomic, copy) NSString *url;
// 請(qǐng)求方法
@property (nonatomic, assign) YTKRequestMethod method;
// 請(qǐng)求參數(shù)
@property (nonatomic, copy) NSDictionary *parameters;
...
// 原來的請(qǐng)求方法
- (void)startWithCompletedBlock:(EPRequestCompletedCallBack)callBack;
// 帶過濾功能的請(qǐng)求方法
- (void)startWithFilterDuplicateRequestInDuration:(NSTimeInterval)duration completedBlock:(EPRequestCompletedCallBack)callBack;
...
@end
EPRequest對(duì)象,表示一個(gè)請(qǐng)求。如果調(diào)用了第二個(gè)方法,內(nèi)部就會(huì)判斷當(dāng)前的請(qǐng)求是否已經(jīng)開啟了過濾,如果沒開始,就發(fā)出這個(gè)請(qǐng)求,并且開啟過濾;如果已經(jīng)開啟了過濾,就根據(jù)當(dāng)前所處的過濾狀態(tài)做相應(yīng)的處理。
這里應(yīng)該注意的是開啟過濾之后,又調(diào)用相同url請(qǐng)求的 startWithFilterDuplicateRequestInDuration 方法時(shí),不是直接過濾,當(dāng)做沒調(diào)用,因?yàn)樵瓉淼恼?qǐng)求方法調(diào)用之后是有block回調(diào)的,會(huì)返回調(diào)用結(jié)果,如果帶過濾功能的請(qǐng)求方法直接無視,豈不是block里面的邏輯沒執(zhí)行了。此時(shí)的設(shè)計(jì)應(yīng)該是內(nèi)部會(huì)過濾請(qǐng)求,但是對(duì)于業(yè)務(wù)層來說,是無感知的,我們還是要返回正常結(jié)果。這個(gè)結(jié)果從哪里來呢,來自一開始的url請(qǐng)求,我們會(huì)把請(qǐng)求回來后的結(jié)果記錄下來,供過濾掉的請(qǐng)求回調(diào)使用。
所以,根據(jù)過濾時(shí)間設(shè)置的定時(shí)器時(shí)間是否已結(jié)束,以及一開始發(fā)出的請(qǐng)求結(jié)果是否已經(jīng)返回,我們可以梳理出在請(qǐng)求開啟過濾時(shí),可能存在的四種狀態(tài):
(1)定時(shí)器未結(jié)束,結(jié)果未返回
(2)定時(shí)器未結(jié)束,結(jié)果已返回
(3)定時(shí)器已結(jié)束,結(jié)果未返回
(4)定時(shí)器已結(jié)束,結(jié)果已返回
對(duì)于狀態(tài)(1),有需要過濾的請(qǐng)求過來時(shí),應(yīng)該把這個(gè)請(qǐng)求記錄下來,等待第一個(gè)發(fā)出的請(qǐng)求結(jié)果的返回,然后這個(gè)結(jié)果就可以直接給該過濾的請(qǐng)求,讓它返回給外部了。
對(duì)于狀態(tài)(2),有需要過濾的請(qǐng)求過來時(shí),由于此時(shí)第一個(gè)發(fā)出的請(qǐng)求的結(jié)果已經(jīng)返回,我們只需要把這個(gè)結(jié)果返回給該過濾請(qǐng)求的回調(diào)就可以了;對(duì)于業(yè)務(wù)層來說,相當(dāng)于調(diào)用了接口立即有了結(jié)果。
對(duì)于狀態(tài)(3),雖然此時(shí)定時(shí)器已結(jié)束,但這里我還是設(shè)計(jì)為如果一開始的請(qǐng)求結(jié)果未返回,還是視為過濾流程沒有結(jié)束(因?yàn)榇藭r(shí)可能由于網(wǎng)絡(luò)或者服務(wù)器原因而沒有返回,短時(shí)間內(nèi)再發(fā)一次也是徒勞,至少也要等超時(shí)結(jié)果返回再發(fā));此時(shí)也會(huì)記錄該請(qǐng)求,等待第一個(gè)發(fā)出的請(qǐng)求結(jié)果返回,然后這個(gè)結(jié)果給該過濾的請(qǐng)求(超時(shí)結(jié)果也算返回的結(jié)果),然后就進(jìn)入了狀態(tài)(4)。
對(duì)于狀態(tài)(4),顯然狀態(tài)(1)(2)(3)最后都會(huì)進(jìn)入狀態(tài)(4),表示這個(gè)過濾流程已經(jīng)結(jié)束了。此時(shí)有需要過濾的請(qǐng)求過來時(shí),說明這是第一個(gè)請(qǐng)求,不需要過濾,這是一個(gè)真正會(huì)發(fā)出的請(qǐng)求,并且設(shè)置了定時(shí)器,開啟了過濾的流程。
根據(jù)如上的狀態(tài)分析,以及每個(gè)狀態(tài)需要什么操作,畫出的三個(gè)流程圖如下所示:


這樣這個(gè)過濾請(qǐng)求功能的整體設(shè)計(jì)就出來了。但是這里有一點(diǎn)需要注意,就是外部調(diào)用過濾請(qǐng)求的方法時(shí),可能處于不同的線程,也就是說在內(nèi)部代碼的處理上,會(huì)涉及到線程安全的處理。具體來說,我們應(yīng)該在改變狀態(tài)的代碼上加一個(gè)鎖。為什么呢,以狀態(tài)(3)和流程3來舉例,假如有一個(gè)過濾請(qǐng)求進(jìn)來,判斷處于狀態(tài)(3),此時(shí)這個(gè)請(qǐng)求會(huì)被記錄,等待結(jié)果返回時(shí)應(yīng)用該結(jié)果,但此時(shí)流程3在并行執(zhí)行;該操作(請(qǐng)求結(jié)果返回并將所有記錄的請(qǐng)求取出,將請(qǐng)求結(jié)果回調(diào)給它們)發(fā)生在過濾請(qǐng)求的狀態(tài)(3)判斷之后,請(qǐng)求被記錄之前,那么就會(huì)導(dǎo)致這個(gè)請(qǐng)求雖然被記錄,但是永遠(yuǎn)不會(huì)有回調(diào)的時(shí)候。
所以,狀態(tài)判斷的相關(guān)代碼是可以在多個(gè)線程同時(shí)執(zhí)行的,但是狀態(tài)變更的代碼跟狀態(tài)判斷代碼,狀態(tài)變更代碼是互斥的;這個(gè)鎖應(yīng)該是一個(gè)多讀單寫的鎖。
CGD有一個(gè)鎖可以實(shí)現(xiàn)該功能:
// 初始化隊(duì)列
dispatch_queue_t queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
// 讀
dispatch_async(queue, ^{
});
// 寫
dispatch_barrier_async(queue, ^{
});
注意 dispatch_barrier_async 傳入的隊(duì)列必須是一個(gè)自己創(chuàng)建的并發(fā)隊(duì)列,不能是全局的并發(fā)隊(duì)列(如果傳入全局的并發(fā)隊(duì)列,效果等同于調(diào)用了dispatch_async)。
至此,整個(gè)設(shè)計(jì)思路就是這樣了,由于具體實(shí)現(xiàn)代碼是根據(jù)我們的項(xiàng)目的請(qǐng)求類定制的,所以就不貼出來了。功能完成后,上線1個(gè)月,完美運(yùn)行hahaha~。