iOS客戶(hù)端經(jīng)常遇到點(diǎn)擊某個(gè)按鈕發(fā)送一個(gè)請(qǐng)求到服務(wù)器,貌似一個(gè)非常簡(jiǎn)單的需求有的時(shí)候其實(shí)并不是那么簡(jiǎn)單,比如網(wǎng)絡(luò)不好的時(shí)候,用戶(hù)重復(fù)點(diǎn)擊一個(gè)按鈕會(huì)發(fā)送多次請(qǐng)求,比如在我負(fù)責(zé)的客戶(hù)端來(lái)說(shuō)用戶(hù)發(fā)帖功能導(dǎo)致的弊端就是,一個(gè)用戶(hù)對(duì)一個(gè)帖子回復(fù)了很多條,有的時(shí)候甚至達(dá)到了10多條,如何解決這一的問(wèn)題呢。方案其實(shí)有很多。
利用MBProgressHud等控件
眾所周知MBProgressHud或者SVProgresHud經(jīng)常被利用在項(xiàng)目中,主要是在網(wǎng)絡(luò)請(qǐng)求發(fā)起到網(wǎng)絡(luò)相應(yīng)收到的這段時(shí)間在客戶(hù)端形成一個(gè)遮罩,可以用來(lái)阻止用戶(hù)點(diǎn)擊UI進(jìn)行操作,防止某些意外的請(qǐng)求產(chǎn)生。
- 優(yōu)點(diǎn):解決了用戶(hù)重復(fù)點(diǎn)擊多次發(fā)送請(qǐng)求的問(wèn)題,同時(shí)防止了在某些條件不具備的情況進(jìn)行其他操作引發(fā)客戶(hù)端出現(xiàn)問(wèn)題的出現(xiàn)。
- 缺點(diǎn):有的時(shí)候不人性化,比如用戶(hù)進(jìn)入某個(gè)界面就是網(wǎng)速不好,一直請(qǐng)求數(shù)據(jù),等了好長(zhǎng)時(shí)間都沒(méi)有結(jié)果,這個(gè)時(shí)候用戶(hù)一般都會(huì)下意識(shí)點(diǎn)擊返回按鈕,但是這種情況下,返回按鈕的點(diǎn)擊事件也是不起作用的。
利用運(yùn)行時(shí)設(shè)置相應(yīng)按鈕點(diǎn)擊間隔
1. 對(duì)UIControl進(jìn)行擴(kuò)展
該方案來(lái)自http://www.cocoachina.com/ios/20150828/13260.html
@interface UIControl (delay)
@property (nonatomic, assign) NSTimeInterval uxy_acceptEventInterval; // 可以用這個(gè)給重復(fù)點(diǎn)擊加間隔
@end
#import "UIControl+delay.h"
#import <objc/runtime.h>
//增加兩個(gè)屬性
static const char *UIControl_acceptEventInterval = "UIControl_acceptEventInterval";
static const char *UIControl_ignoreEvent = "UIControl_ignoreEvent";
@implementation UIControl (delay)
//時(shí)間間隔
- (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方法和自己實(shí)現(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)擊后會(huì)先進(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];
}
//這里其實(shí)是系統(tǒng)的原來(lái)的sendAction to方法。
[self __uxy_sendAction:action to:target forEvent:event];
}
@end
2.對(duì)UIButton進(jìn)行擴(kuò)展
該方案來(lái)自 http://www.tuicool.com/articles/NJvmIf
這個(gè)在點(diǎn)擊UITabbar上的按鈕時(shí)會(huì)崩潰,提示
-[UITabBarButton cs_acceptEventTime]: unrecognized selector sent to instance 0x7fc9d8f36c50,自己找了好久都沒(méi)有找到原因,后來(lái)參考
http://blog.jobbole.com/79580/ 改寫(xiě)了load方法就好了,原因不明白一直不明白,UIButton繼承UIControl應(yīng)該沒(méi)有什么問(wèn)題,為什么UITabbarButton會(huì)出錯(cuò)呢。方案一對(duì)UIControl進(jìn)行擴(kuò)展,在load方法里面直接進(jìn)行了交換,是因?yàn)閁IControl的sendAction:to:event方法確實(shí)是存在的,也許UITabbarButton有特殊的地方吧,就是沒(méi)有這個(gè)方法。
@implementation UIButton (delay)
// 因category不能添加屬性,只能通過(guò)關(guān)聯(lián)對(duì)象的方式。
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時(shí)執(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);
//先嘗試給原來(lái)的方法添加實(shí)現(xiàn),如果原來(lái)的方法不存在就可以添加成功。返回為YES,否則
//返回為NO。
//UIButton 真的沒(méi)有sendAction方法的實(shí)現(xiàn),這是繼承了UIControl的而已,UIControl才真正的實(shí)現(xiàn)了。
BOOL didAddMethod =
class_addMethod(class,
beforeSelector,
method_getImplementation(afterMethod),
method_getTypeEncoding(afterMethod));
NSLog(@"%d",didAddMethod);
if (didAddMethod) {
// 如果之前不存在,但是添加成功了,此時(shí)添加成功的是cs_sendAction方法的實(shí)現(xiàn)
// 這里只需要方法替換
class_replaceMethod(class,
afterSelector,
method_getImplementation(beforeMethod),
method_getTypeEncoding(beforeMethod));
} else {
//本來(lái)如果存在就進(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):有效解決了用戶(hù)雙擊UI造成事件觸發(fā)兩次的情況(不僅僅局限網(wǎng)絡(luò)請(qǐng)求)/
- 缺點(diǎn) :在網(wǎng)絡(luò)不好的情況下,很可能在m秒內(nèi)確實(shí)沒(méi)有收到服務(wù)器響應(yīng)。如果用戶(hù)一直點(diǎn)擊按鈕,很可能觸發(fā)重復(fù)點(diǎn)擊。而且可能和系統(tǒng)以及存在的事件沖突,有的時(shí)候會(huì)產(chǎn)生莫名其妙的錯(cuò)誤。比如我加入這個(gè)類(lèi)擴(kuò)展后,項(xiàng)目中選擇照片時(shí)候進(jìn)行拍照上傳的時(shí)候,本來(lái)需要點(diǎn)擊一下拍攝按鈕就可以成功的事情,確需要長(zhǎng)時(shí)間觸摸才能生效,所以這個(gè)方案待改進(jìn)。
客戶(hù)端網(wǎng)絡(luò)請(qǐng)求方法中過(guò)濾
一個(gè)網(wǎng)絡(luò)請(qǐng)求包含兩部分:url和參數(shù),因此我們可以在網(wǎng)絡(luò)請(qǐng)求方類(lèi)里面增加一個(gè)NSMutableArray,用戶(hù)url和參數(shù)的md5進(jìn)行一次加密作為key,發(fā)送之前我們可以對(duì)其值賦值為任意固定值,當(dāng)服務(wù)器返回結(jié)果的時(shí)候我們可以將這個(gè)鍵值對(duì)移除。每次發(fā)送網(wǎng)絡(luò)請(qǐng)求前,先從這個(gè)字典中查看本次請(qǐng)求的md5值是否存在,如果存在表明本次請(qǐng)求已經(jīng)發(fā)送但是尚未收到響應(yīng),此時(shí)應(yīng)該return,不再進(jìn)行網(wǎng)絡(luò)請(qǐng)求,否則就是收到響應(yīng)了或者該請(qǐng)求是第一次發(fā)出,改方法貌似不錯(cuò)。
注意:有的時(shí)候參數(shù)包含了時(shí)間戳,這樣計(jì)算永遠(yuǎn)會(huì)不相同的,md5加密之前要清除參數(shù)中的時(shí)間戳或者隨機(jī)字段。
交給服務(wù)器解決
上面的辦法都是客戶(hù)端進(jìn)行解決的,其實(shí)仔細(xì)想想這個(gè)問(wèn)題服務(wù)器端難道就能完全沒(méi)有責(zé)任嗎?顯然不是! 比如有人惡意模仿客戶(hù)端模擬頻繁向服務(wù)器發(fā)出http請(qǐng)求,這勢(shì)必會(huì)造成服務(wù)器端資源浪費(fèi),雖然說(shuō)http協(xié)議是不能記住狀態(tài)的(需要靠session技術(shù)實(shí)現(xiàn)),但是服務(wù)器對(duì)這樣的行為就束手無(wú)策,顯然是不符合常理的。介于本人對(duì)服務(wù)器的技術(shù)了解有限,所以感覺(jué)應(yīng)該上一種解決方案里面的客戶(hù)端實(shí)現(xiàn)的過(guò)濾加入到服務(wù)器端實(shí)現(xiàn),基本和客戶(hù)端一致。
具體方案參考:
服務(wù)器把每次把收到的請(qǐng)求進(jìn)行MD5加密,作為一個(gè)字典的鍵,值可以設(shè)置任意,然后查找數(shù)據(jù)庫(kù),查找回來(lái)以后通過(guò)適當(dāng)?shù)男问椒祷乜蛻?hù)端,在查找數(shù)據(jù)期間,收到請(qǐng)求先從字典查找鍵是否存在如果已經(jīng)存在就不作出響應(yīng),因?yàn)檎诓檎抑?,否則操作數(shù)據(jù)庫(kù)查找數(shù)據(jù),并且將鏈接鍵入到字典里面。
上述方案是本人工作中的思考還有互聯(lián)網(wǎng)上查找的方案總結(jié),難免有不足之處,僅供參考。希望各位能夠提供更好的解決方案,歡迎留言。