iOS客户端经常遇到点击某个按钮发送一个请求到服务器,貌似一个非常简单的需求有的时候其实并不是那么简单,比如网络不好的时候,用户重复点击一个按钮会发送多次请求,比如在我负责的客户端来说用户发帖功能导致的弊端就是,一个用户对一个帖子回复了很多条,有的时候甚至达到了10多条,如何解决这一的问题呢。方案其实有很多。
利用MBProgressHud等控件
众所周知MBProgressHud或者SVProgresHud经常被利用在项目中,主要是在网络请求发起到网络相应收到的这段时间在客户端形成一个遮罩,可以用来阻止用户点击UI进行操作,防止某些意外的请求产生。
- 优点:解决了用户重复点击多次发送请求的问题,同时防止了在某些条件不具备的情况进行其他操作引发客户端出现问题的出现。
- 缺点:有的时候不人性化,比如用户进入某个界面就是网速不好,一直请求数据,等了好长时间都没有结果,这个时候用户一般都会下意识点击返回按钮,但是这种情况下,返回按钮的点击事件也是不起作用的。
利用运行时设置相应按钮点击间隔
1. 对UIControl进行扩展
该方案来自http://www.cocoachina.com/ios/20150828/13260.html
@interface UIControl (delay)
@property (nonatomic, assign) NSTimeInterval uxy_acceptEventInterval; // 可以用这个给重复点击加间隔
@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);
}
//是否响应事件的标志位
-(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
{
//将系统的sendAction方法和自己实现的方法进行互换
Method a=class_getInstanceMethod(self,@selector(sendAction:to:forEvent:));
Method b = class_getInstanceMethod(self,@selector(__uxy_sendAction:to:forEvent:));
method_exchangeImplementations(a,b);
}
//点击后会先进入这里
- (void)__uxy_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
if (self.uxy_ignoreEvent)//根据状态判断是否继续执行
return;
if (self.uxy_acceptEventInterval > 0)
{
self.uxy_ignoreEvent = YES;
//周期性清空标志位
[self performSelector:@selector(setUxy_ignoreEvent:) withObject:@(NO) afterDelay:self.uxy_acceptEventInterval];
}
//这里其实是系统的原来的sendAction to方法。
[self __uxy_sendAction:action to:target forEvent:event];
}
@end
2.对UIButton进行扩展
该方案来自 http://www.tuicool.com/articles/NJvmIf
这个在点击UITabbar上的按钮时会崩溃,提示
-[UITabBarButton cs_acceptEventTime]: unrecognized selector sent to instance 0x7fc9d8f36c50
,自己找了好久都没有找到原因,后来参考
http://blog.jobbole.com/79580/ 改写了load方法就好了,原因不明白一直不明白,UIButton继承UIControl应该没有什么问题,为什么UITabbarButton会出错呢。方案一对UIControl进行扩展,在load方法里面直接进行了交换,是因为UIControl的sendAction:to:event方法确实是存在的,也许UITabbarButton有特殊的地方吧
,就是没有这个方法。
@implementation UIButton (delay)
// 因category不能添加属性,只能通过关联对象的方式。
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时执行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);
//先尝试给原来的方法添加实现,如果原来的方法不存在就可以添加成功。返回为YES,否则
//返回为NO。
//UIButton 真的没有sendAction方法的实现,这是继承了UIControl的而已,UIControl才真正的实现了。
BOOL didAddMethod =
class_addMethod(class,
beforeSelector,
method_getImplementation(afterMethod),
method_getTypeEncoding(afterMethod));
NSLog(@"%d",didAddMethod);
if (didAddMethod) {
// 如果之前不存在,但是添加成功了,此时添加成功的是cs_sendAction方法的实现
// 这里只需要方法替换
class_replaceMethod(class,
afterSelector,
method_getImplementation(beforeMethod),
method_getTypeEncoding(beforeMethod));
} else {
//本来如果存在就进行交换
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
- 优点:有效解决了用户双击UI造成事件触发两次的情况(不仅仅局限网络请求)/
- 缺点 :在网络不好的情况下,很可能在m秒内确实没有收到服务器响应。如果用户一直点击按钮,很可能触发重复点击。而且可能和系统以及存在的事件冲突,有的时候会产生莫名其妙的错误。比如我加入这个类扩展后,项目中选择照片时候进行拍照上传的时候,本来需要点击一下拍摄按钮就可以成功的事情,确需要长时间触摸才能生效,所以这个方案待改进。
客户端网络请求方法中过滤
一个网络请求包含两部分:url
和参数
,因此我们可以在网络请求方类里面增加一个NSMutableArray,用户url
和参数
的md5进行一次加密作为key
,发送之前我们可以对其值赋值为任意固定值,当服务器返回结果的时候我们可以将这个键值对移除。每次发送网络请求前,先从这个字典中查看本次请求的md5值是否存在,如果存在表明本次请求已经发送但是尚未收到响应,此时应该return,不再进行网络请求,否则就是收到响应了或者该请求是第一次发出,改方法貌似不错
。
注意:有的时候参数包含了时间戳,这样计算永远会不相同的,md5加密之前要清除参数中的时间戳或者随机字段。
交给服务器解决
上面的办法都是客户端进行解决的,其实仔细想想这个问题服务器端难道就能完全没有责任吗?显然不是! 比如有人恶意模仿客户端模拟频繁向服务器发出http请求,这势必会造成服务器端资源浪费,虽然说http协议是不能记住状态的(需要靠session技术实现),但是服务器对这样的行为就束手无策,显然是不符合常理的。介于本人对服务器的技术了解有限,所以感觉应该上一种解决方案里面的客户端实现的过滤加入到服务器端实现,基本和客户端一致。
具体方案参考:
服务器把每次把收到的请求进行MD5加密,作为一个字典的键,值可以设置任意,然后查找数据库,查找回来以后通过适当的形式返回客户端,在查找数据期间,收到请求先从字典查找键是否存在如果已经存在就不作出响应,因为正在查找中,否则操作数据库查找数据,并且将链接键入到字典里面。
上述方案是本人工作中的思考还有互联网上查找的方案总结,难免有不足之处,仅供参考。希望各位能够提供更好的解决方案,欢迎留言。