- 存在的問題
最近在做項目的過程中沮明,發(fā)生APP啟動的時候,有些接口會在短時間(例如1s)之內(nèi)重復(fù)調(diào)用多次窍奋,在APP執(zhí)行某些操作時荐健,也時不時出現(xiàn)一個接口短時間內(nèi)調(diào)用多次的情況。究其原因琳袄,有些請求重復(fù)是因為在不同的時機(jī)點(diǎn)都需要請求這個接口江场,但是有時候這些不同時機(jī)點(diǎn),在短時間內(nèi)會一起發(fā)生窖逗,例如如下這種情況:
- (void)applicationDidBecomeActive:(UIApplication *)application {
// 請求接口
}
// 首頁控制器
- (void)viewDidLoad {
// 請求接口
}
我需要在app回到前臺時請求接口址否,在首頁控制器加載完后也需要請求接口,這兩個不同的時機(jī)碎紊,會在APP啟動時短時間內(nèi)都發(fā)生佑附,導(dǎo)致接口重復(fù)請求了。雖然可以針對這種情況寫一些判斷代碼仗考,當(dāng)每發(fā)生類似情況都寫一次判斷就不優(yōu)雅了音同。
還有些重復(fù)請求的情況是代碼一開始設(shè)計沒有考慮那么完善,后面代碼迭代痴鳄,就會導(dǎo)致有些邏輯重復(fù)走幾次瘟斜,接口也重復(fù)請求了。這種改起來就要小心翼翼了痪寻,一不小心就容易改出bug螺句。
- 解決問題的思路
所以仔細(xì)思考后,還是決定統(tǒng)一處理橡类,從請求的底層下手蛇尚,在請求的底層做重復(fù)請求的管理,提供一個過濾方法給業(yè)務(wù)層顾画,業(yè)務(wù)層只需要把原來的請求接口的方法替換成有過濾功能的方法就可以了取劫,不需要改動業(yè)務(wù)邏輯匆笤。
那么應(yīng)該如何設(shè)計這個對外提供的方法呢,我這里設(shè)計為方法會提供一個參數(shù)谱邪,可以設(shè)置過濾時間炮捧,在過濾時間范圍內(nèi),相同的請求只會發(fā)出去一次:
@interface EPRequest : YTKRequest<NSCoding>
// url
@property (nonatomic, copy) NSString *url;
// 請求方法
@property (nonatomic, assign) YTKRequestMethod method;
// 請求參數(shù)
@property (nonatomic, copy) NSDictionary *parameters;
...
// 原來的請求方法
- (void)startWithCompletedBlock:(EPRequestCompletedCallBack)callBack;
// 帶過濾功能的請求方法
- (void)startWithFilterDuplicateRequestInDuration:(NSTimeInterval)duration completedBlock:(EPRequestCompletedCallBack)callBack;
...
@end
EPRequest對象惦银,表示一個請求咆课。如果調(diào)用了第二個方法,內(nèi)部就會判斷當(dāng)前的請求是否已經(jīng)開啟了過濾扯俱,如果沒開始书蚪,就發(fā)出這個請求,并且開啟過濾迅栅;如果已經(jīng)開啟了過濾殊校,就根據(jù)當(dāng)前所處的過濾狀態(tài)做相應(yīng)的處理。
這里應(yīng)該注意的是開啟過濾之后读存,又調(diào)用相同url請求的 startWithFilterDuplicateRequestInDuration 方法時为流,不是直接過濾,當(dāng)做沒調(diào)用宪萄,因為原來的請求方法調(diào)用之后是有block回調(diào)的艺谆,會返回調(diào)用結(jié)果榨惰,如果帶過濾功能的請求方法直接無視拜英,豈不是block里面的邏輯沒執(zhí)行了。此時的設(shè)計應(yīng)該是內(nèi)部會過濾請求琅催,但是對于業(yè)務(wù)層來說居凶,是無感知的,我們還是要返回正常結(jié)果藤抡。這個結(jié)果從哪里來呢侠碧,來自一開始的url請求,我們會把請求回來后的結(jié)果記錄下來缠黍,供過濾掉的請求回調(diào)使用弄兜。
所以,根據(jù)過濾時間設(shè)置的定時器時間是否已結(jié)束瓷式,以及一開始發(fā)出的請求結(jié)果是否已經(jīng)返回替饿,我們可以梳理出在請求開啟過濾時,可能存在的四種狀態(tài):
(1)定時器未結(jié)束贸典,結(jié)果未返回
(2)定時器未結(jié)束视卢,結(jié)果已返回
(3)定時器已結(jié)束,結(jié)果未返回
(4)定時器已結(jié)束廊驼,結(jié)果已返回
對于狀態(tài)(1)据过,有需要過濾的請求過來時惋砂,應(yīng)該把這個請求記錄下來,等待第一個發(fā)出的請求結(jié)果的返回绳锅,然后這個結(jié)果就可以直接給該過濾的請求西饵,讓它返回給外部了。
對于狀態(tài)(2)鳞芙,有需要過濾的請求過來時罗标,由于此時第一個發(fā)出的請求的結(jié)果已經(jīng)返回,我們只需要把這個結(jié)果返回給該過濾請求的回調(diào)就可以了积蜻;對于業(yè)務(wù)層來說闯割,相當(dāng)于調(diào)用了接口立即有了結(jié)果。
對于狀態(tài)(3)竿拆,雖然此時定時器已結(jié)束宙拉,但這里我還是設(shè)計為如果一開始的請求結(jié)果未返回,還是視為過濾流程沒有結(jié)束(因為此時可能由于網(wǎng)絡(luò)或者服務(wù)器原因而沒有返回丙笋,短時間內(nèi)再發(fā)一次也是徒勞谢澈,至少也要等超時結(jié)果返回再發(fā));此時也會記錄該請求御板,等待第一個發(fā)出的請求結(jié)果返回锥忿,然后這個結(jié)果給該過濾的請求(超時結(jié)果也算返回的結(jié)果),然后就進(jìn)入了狀態(tài)(4)怠肋。
對于狀態(tài)(4)敬鬓,顯然狀態(tài)(1)(2)(3)最后都會進(jìn)入狀態(tài)(4),表示這個過濾流程已經(jīng)結(jié)束了笙各。此時有需要過濾的請求過來時钉答,說明這是第一個請求,不需要過濾杈抢,這是一個真正會發(fā)出的請求数尿,并且設(shè)置了定時器,開啟了過濾的流程惶楼。
根據(jù)如上的狀態(tài)分析右蹦,以及每個狀態(tài)需要什么操作,畫出的三個流程圖如下所示:
這樣這個過濾請求功能的整體設(shè)計就出來了歼捐。但是這里有一點(diǎn)需要注意何陆,就是外部調(diào)用過濾請求的方法時,可能處于不同的線程窥岩,也就是說在內(nèi)部代碼的處理上甲献,會涉及到線程安全的處理。具體來說颂翼,我們應(yīng)該在改變狀態(tài)的代碼上加一個鎖晃洒。為什么呢慨灭,以狀態(tài)(3)和流程3來舉例,假如有一個過濾請求進(jìn)來球及,判斷處于狀態(tài)(3)氧骤,此時這個請求會被記錄,等待結(jié)果返回時應(yīng)用該結(jié)果吃引,但此時流程3在并行執(zhí)行筹陵;該操作(請求結(jié)果返回并將所有記錄的請求取出,將請求結(jié)果回調(diào)給它們)發(fā)生在過濾請求的狀態(tài)(3)判斷之后镊尺,請求被記錄之前朦佩,那么就會導(dǎo)致這個請求雖然被記錄,但是永遠(yuǎn)不會有回調(diào)的時候庐氮。
所以语稠,狀態(tài)判斷的相關(guān)代碼是可以在多個線程同時執(zhí)行的,但是狀態(tài)變更的代碼跟狀態(tài)判斷代碼弄砍,狀態(tài)變更代碼是互斥的仙畦;這個鎖應(yīng)該是一個多讀單寫的鎖。
CGD有一個鎖可以實現(xiàn)該功能:
// 初始化隊列
dispatch_queue_t queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
// 讀
dispatch_async(queue, ^{
});
// 寫
dispatch_barrier_async(queue, ^{
});
注意 dispatch_barrier_async 傳入的隊列必須是一個自己創(chuàng)建的并發(fā)隊列音婶,不能是全局的并發(fā)隊列(如果傳入全局的并發(fā)隊列慨畸,效果等同于調(diào)用了dispatch_async)。
至此衣式,整個設(shè)計思路就是這樣了寸士,由于具體實現(xiàn)代碼是根據(jù)我們的項目的請求類定制的,所以就不貼出來了瞳收。功能完成后碉京,上線1個月厢汹,完美運(yùn)行hahaha~螟深。