iOS客戶端經(jīng)常遇到點(diǎn)擊某個按鈕發(fā)送一個請求到服務(wù)器热凹,貌似一個非常簡單的需求有的時候其實并不是那么簡單,比如網(wǎng)絡(luò)不好的時候颇玷,用戶重復(fù)點(diǎn)擊一個按鈕會發(fā)送多次請求陵究,比如在我負(fù)責(zé)的客戶端來說用戶發(fā)帖功能導(dǎo)致的弊端就是辉词,一個用戶對一個帖子回復(fù)了很多條禽绪,有的時候甚至達(dá)到了10多條医瘫,如何解決這一的問題呢侣肄。方案其實有很多。
利用MBProgressHud等控件
眾所周知MBProgressHud或者SVProgresHud經(jīng)常被利用在項目中醇份,主要是在網(wǎng)絡(luò)請求發(fā)起到網(wǎng)絡(luò)相應(yīng)收到的這段時間在客戶端形成一個遮罩稼锅,可以用來阻止用戶點(diǎn)擊UI進(jìn)行操作,防止某些意外的請求產(chǎn)生僚纷。
- 優(yōu)點(diǎn):解決了用戶重復(fù)點(diǎn)擊多次發(fā)送請求的問題矩距,同時防止了在某些條件不具備的情況進(jìn)行其他操作引發(fā)客戶端出現(xiàn)問題的出現(xiàn)。
- 缺點(diǎn):有的時候不人性化怖竭,比如用戶進(jìn)入某個界面就是網(wǎng)速不好锥债,一直請求數(shù)據(jù),等了好長時間都沒有結(jié)果,這個時候用戶一般都會下意識點(diǎn)擊返回按鈕哮肚,但是這種情況下登夫,返回按鈕的點(diǎn)擊事件也是不起作用的。
利用運(yùn)行時設(shè)置相應(yīng)按鈕點(diǎn)擊間隔
1. 對UIControl進(jìn)行擴(kuò)展
該方案來自http://www.cocoachina.com/ios/20150828/13260.html
@interface UIControl (delay)
@property (nonatomic, assign) NSTimeInterval uxy_acceptEventInterval; // 可以用這個給重復(fù)點(diǎn)擊加間隔
@end
#import "UIControl+delay.h"
#import <objc/runtime.h>
//增加兩個屬性
static const char *UIControl_acceptEventInterval = "UIControl_acceptEventInterval";
static const char *UIControl_ignoreEvent = "UIControl_ignoreEvent";
@implementation UIControl (delay)
//時間間隔
- (NSTimeInterval)uxy_acceptEventInterval
{
return [objc_getAssociatedObject(self, UIControl_acceptEventInterval) doubleValue];
}
- (void)setUxy_acceptEventInterval:(NSTimeInterval)uxy_acceptEventInterval
{
objc_setAssociatedObject(self, UIControl_acceptEventInterval, @(uxy_acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
//是否響應(yīng)事件的標(biāo)志位
-(BOOL)uxy_ignoreEvent
{
return [objc_getAssociatedObject(self, UIControl_ignoreEvent) boolValue];
}
-(void)setUxy_ignoreEvent:(BOOL)uxy_ignoreEvent
{
objc_setAssociatedObject(self, UIControl_ignoreEvent, @(uxy_ignoreEvent), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
+(void)load
{
//將系統(tǒng)的sendAction方法和自己實現(xiàn)的方法進(jìn)行互換
Method a=class_getInstanceMethod(self,@selector(sendAction:to:forEvent:));
Method b = class_getInstanceMethod(self,@selector(__uxy_sendAction:to:forEvent:));
method_exchangeImplementations(a,b);
}
//點(diǎn)擊后會先進(jìn)入這里
- (void)__uxy_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
if (self.uxy_ignoreEvent)//根據(jù)狀態(tài)判斷是否繼續(xù)執(zhí)行
return;
if (self.uxy_acceptEventInterval > 0)
{
self.uxy_ignoreEvent = YES;
//周期性清空標(biāo)志位
[self performSelector:@selector(setUxy_ignoreEvent:) withObject:@(NO) afterDelay:self.uxy_acceptEventInterval];
}
//這里其實是系統(tǒng)的原來的sendAction to方法允趟。
[self __uxy_sendAction:action to:target forEvent:event];
}
@end
2.對UIButton進(jìn)行擴(kuò)展
該方案來自 http://www.tuicool.com/articles/NJvmIf
這個在點(diǎn)擊UITabbar上的按鈕時會崩潰恼策,提示
-[UITabBarButton cs_acceptEventTime]: unrecognized selector sent to instance 0x7fc9d8f36c50
,自己找了好久都沒有找到原因潮剪,后來參考
http://blog.jobbole.com/79580/ 改寫了load方法就好了涣楷,原因不明白一直不明白,UIButton繼承UIControl應(yīng)該沒有什么問題抗碰,為什么UITabbarButton會出錯呢总棵。方案一對UIControl進(jìn)行擴(kuò)展,在load方法里面直接進(jìn)行了交換改含,是因為UIControl的sendAction:to:event方法確實是存在的,也許UITabbarButton有特殊的地方吧
,就是沒有這個方法迄汛。
@implementation UIButton (delay)
// 因category不能添加屬性捍壤,只能通過關(guān)聯(lián)對象的方式。
static const char *UIControl_acceptEventInterval = "UIControl_acceptEventInterval";
- (NSTimeInterval)cs_acceptEventInterval {
return [objc_getAssociatedObject(self, UIControl_acceptEventInterval) doubleValue];
}
- (void)setCs_acceptEventInterval:(NSTimeInterval)cs_acceptEventInterval {
objc_setAssociatedObject(self, UIControl_acceptEventInterval, @(cs_acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
static const char *UIControl_acceptEventTime = "UIControl_acceptEventTime";
- (NSTimeInterval)cs_acceptEventTime {
return [objc_getAssociatedObject(self, UIControl_acceptEventTime) doubleValue];
}
- (void)setCs_acceptEventTime:(NSTimeInterval)cs_acceptEventTime {
objc_setAssociatedObject(self, UIControl_acceptEventTime, @(cs_acceptEventTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
// 在load時執(zhí)行hook
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
//分別獲取
SEL beforeSelector = @selector(sendAction:to:forEvent:);
SEL afterSelector = @selector(cs_sendAction:to:forEvent:);
Method beforeMethod = class_getInstanceMethod(class, beforeSelector);
Method afterMethod = class_getInstanceMethod(class, afterSelector);
//先嘗試給原來的方法添加實現(xiàn)鞍爱,如果原來的方法不存在就可以添加成功鹃觉。返回為YES,否則
//返回為NO睹逃。
//UIButton 真的沒有sendAction方法的實現(xiàn)盗扇,這是繼承了UIControl的而已,UIControl才真正的實現(xiàn)了沉填。
BOOL didAddMethod =
class_addMethod(class,
beforeSelector,
method_getImplementation(afterMethod),
method_getTypeEncoding(afterMethod));
NSLog(@"%d",didAddMethod);
if (didAddMethod) {
// 如果之前不存在疗隶,但是添加成功了,此時添加成功的是cs_sendAction方法的實現(xiàn)
// 這里只需要方法替換
class_replaceMethod(class,
afterSelector,
method_getImplementation(beforeMethod),
method_getTypeEncoding(beforeMethod));
} else {
//本來如果存在就進(jìn)行交換
method_exchangeImplementations(afterMethod, beforeMethod);
}
});
}
- (void)cs_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
if ([NSDate date].timeIntervalSince1970 - self.cs_acceptEventTime < self.cs_acceptEventInterval) {
return;
}
if (self.cs_acceptEventInterval > 0) {
self.cs_acceptEventTime = [NSDate date].timeIntervalSince1970;
}
[self cs_sendAction:action to:target forEvent:event];
}
@end
- 優(yōu)點(diǎn):有效解決了用戶雙擊UI造成事件觸發(fā)兩次的情況(不僅僅局限網(wǎng)絡(luò)請求)/
- 缺點(diǎn) :在網(wǎng)絡(luò)不好的情況下翼闹,很可能在m秒內(nèi)確實沒有收到服務(wù)器響應(yīng)斑鼻。如果用戶一直點(diǎn)擊按鈕,很可能觸發(fā)重復(fù)點(diǎn)擊猎荠。而且可能和系統(tǒng)以及存在的事件沖突坚弱,有的時候會產(chǎn)生莫名其妙的錯誤。比如我加入這個類擴(kuò)展后关摇,項目中選擇照片時候進(jìn)行拍照上傳的時候荒叶,本來需要點(diǎn)擊一下拍攝按鈕就可以成功的事情,確需要長時間觸摸才能生效,所以這個方案待改進(jìn)输虱。
客戶端網(wǎng)絡(luò)請求方法中過濾
一個網(wǎng)絡(luò)請求包含兩部分:url
和參數(shù)
,因此我們可以在網(wǎng)絡(luò)請求方類里面增加一個NSMutableArray些楣,用戶url
和參數(shù)
的md5進(jìn)行一次加密作為key
,發(fā)送之前我們可以對其值賦值為任意固定值,當(dāng)服務(wù)器返回結(jié)果的時候我們可以將這個鍵值對移除戈毒。每次發(fā)送網(wǎng)絡(luò)請求前艰猬,先從這個字典中查看本次請求的md5值是否存在,如果存在表明本次請求已經(jīng)發(fā)送但是尚未收到響應(yīng)埋市,此時應(yīng)該return冠桃,不再進(jìn)行網(wǎng)絡(luò)請求,否則就是收到響應(yīng)了或者該請求是第一次發(fā)出道宅,改方法貌似不錯
食听。
注意:有的時候參數(shù)包含了時間戳,這樣計算永遠(yuǎn)會不相同的污茵,md5加密之前要清除參數(shù)中的時間戳或者隨機(jī)字段樱报。
交給服務(wù)器解決
上面的辦法都是客戶端進(jìn)行解決的,其實仔細(xì)想想這個問題服務(wù)器端難道就能完全沒有責(zé)任嗎?顯然不是! 比如有人惡意模仿客戶端模擬頻繁向服務(wù)器發(fā)出http請求泞当,這勢必會造成服務(wù)器端資源浪費(fèi)迹蛤,雖然說http協(xié)議是不能記住狀態(tài)的(需要靠session技術(shù)實現(xiàn)),但是服務(wù)器對這樣的行為就束手無策襟士,顯然是不符合常理的盗飒。介于本人對服務(wù)器的技術(shù)了解有限,所以感覺應(yīng)該上一種解決方案里面的客戶端實現(xiàn)的過濾加入到服務(wù)器端實現(xiàn)陋桂,基本和客戶端一致逆趣。
具體方案參考:
服務(wù)器把每次把收到的請求進(jìn)行MD5加密,作為一個字典的鍵嗜历,值可以設(shè)置任意宣渗,然后查找數(shù)據(jù)庫,查找回來以后通過適當(dāng)?shù)男问椒祷乜蛻舳死嬷荩诓檎覕?shù)據(jù)期間痕囱,收到請求先從字典查找鍵是否存在如果已經(jīng)存在就不作出響應(yīng),因為正在查找中摊唇,否則操作數(shù)據(jù)庫查找數(shù)據(jù)咐蝇,并且將鏈接鍵入到字典里面。
上述方案是本人工作中的思考還有互聯(lián)網(wǎng)上查找的方案總結(jié)巷查,難免有不足之處有序,僅供參考。希望各位能夠提供更好的解決方案岛请,歡迎留言旭寿。