Runtime应用防止按钮连续点击 (转)
好久之前就看到过使用Runtime解决按钮的连续点击的问题,一直觉得没啥好记录的。刚好今天旁边同时碰到这个问题,看他们好捉急而且好像很难处理,于是我先自己看看…
前面自己也学习了很多Runtime的东西,一直觉得这个按钮连续点击其实很简单,就使用Runtime交换SEL实现IMP即可,但其实没明白解决这个问题的过程.
虽然直接可以在github搜到解决方法,但是还是有必要学习一下解决这个问题的一步一步的思路,给出这个作者的git:
https://github.com/strivever/UIButton-touch
@implementation ViewController - (void)btnDidClick:(id)sender {
NSLog(@"我被点击了....");
} - (void)viewDidLoad {
[super viewDidLoad]; MyButton *btn = [[MyButton alloc] init];
[btn setTitle:@"点我啊" forState:UIControlStateNormal];
[btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
btn.layer.borderWidth = ;
btn.frame = CGRectMake(, , , );
[self.view addSubview:btn]; [btn addTarget:self action:@selector(btnDidClick:) forControlEvents:UIControlEventTouchUpInside];
}
如上代码是最简单的UIBUtton使用代码,但是有一个问题就是,按钮可以无限制、没有间隔时间、连续的n次点击都会触发处理函数.
iOS中的按钮事件机制 >>> Target-Action机制
- 用户点击时,产生一个按钮点击事件消息
- 这个消息发送给注册的Target处理
- Target接收到消息,然后查找自己的SEL对应的具体实现IMP正儿八经的去处理点击事件
实际上该点击消息包含三个东西:
- Target处理者
- SEL方法Id
- 按钮事件当时触发时的状态
所有的按钮事件状态 typedef NS_OPTIONS(NSUInteger, UIControlState) {
UIControlStateNormal = ,
UIControlStateHighlighted = << , // used when UIControl isHighlighted is set
UIControlStateDisabled = << ,
UIControlStateSelected = << , // flag usable by app (see below)
UIControlStateFocused NS_ENUM_AVAILABLE_IOS(9_0) = << , // Applicable only when the screen supports focus
UIControlStateApplication = 0x00FF0000, // additional flags available for application use
UIControlStateReserved = 0xFF000000 // flags reserved for internal framework use
};
已经知道点击按钮时候,会产生一个包装了Target、SEL、按钮事件状态三个东西的消息发送给Target处理
问题: 是谁来包装UIButton的点击事件消息,并且完成发送消息了?
这个是解决连续点击按钮的关键问题所在,必须搞清楚。因为如果搞清楚具体包装和发送按钮点击时间消息的地方和时机,那么可以拦截这个地方执行,然后加入是否在指定的间隔时间内决定是否让其继续执行发送消息的操作。
那么问题不就解决了吗,我都不让他发送消息了,他还能执行?
首先从UIButton.h头文件中查找,是否有send message
、send Action
…等等包含send
的方法
无法找到.
UIButton继承自UIControl,而UIControl又负责很多的UI事件处理,那么可以继续从UIControl.h中查找
找到两个send相关的函数:
// send the action. the first method is called for the event and is a point at which you can observe or override behavior. it is called repeately by the second.
- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;
- (void)sendActionsForControlEvents:(UIControlEvents)controlEvents; // send all actions associated with events
没看懂注释有什么意思,那么代码直接试把.
前面我用自己的UIButton子类是有原因的,可以重写父类方法完成父类方法Hook的效果.
尝试进行hook UIControl的 sendAction:to: forEvent:
#import <UIKit/UIKit.h> @interface MyButton : UIButton @end
#import "MyButton.h" @implementation MyButton - (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event { NSLog(@"Pre sendAction >>>> action = %@", NSStringFromSelector(action)); [super sendAction:action to:target forEvent:event]; NSLog(@"After sendAction >>>> action = %@", NSStringFromSelector(action));
} @end
然后在ViewController中也进行下修改,确定按钮响应函数与这个sendAction:to: forEvent:
执行的顺序.
@implementation ViewController - (void)btnDidClick:(id)sender {
NSLog(@"我被点击了 >>> %@", NSStringFromSelector(_cmd));
} - (void)viewDidLoad {
[super viewDidLoad]; MyButton *btn = [[MyButton alloc] init];
[btn setTitle:@"点我啊" forState:UIControlStateNormal];
[btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
btn.layer.borderWidth = ;
btn.frame = CGRectMake(, , , );
[self.view addSubview:btn]; [btn addTarget:self action:@selector(btnDidClick:) forControlEvents:UIControlEventTouchUpInside];
}
最后输出结果如下
-- ::14.181 RuntimeDemo[:] Pre sendAction >>>> action = btnDidClick:
-- ::14.183 RuntimeDemo[:] 我被点击了 >>> btnDidClick:
-- ::14.183 RuntimeDemo[:] After sendAction >>>> action = btnDidClick:
从如上的输出结果,分析一下:
当点击按钮时,立刻执行我们自己MyButton的
sendAction:to: forEvent:
方法实现当继续执行
[UIControl sendAction:to: forEvent:]
时,就会完成如下工作,将流程走到ViewController对象这个Target- 按钮点击事件的消息包装
- 发送给消息处理这Target
当Target接收到消息,进行处理
- 即执行ViewController对象的
btnDidClick:
- 即执行ViewController对象的
当最后Target处理完消息,继续执行
[super sendAction:action to:target forEvent:event];
后面的一句打印
OK,理清楚从按钮点击
~ 消息包装与发送
~ 消息处理
这三个步骤,那么防止按钮连续点击就有突破口了.
最后摘录自来源文字关于UIControl的
sendAction:to:forEvent:
这个方法的作用:
对于一个给定的事件,UIControl会调用sendAction:to:forEvent:来将行为消息转发到
UIApplication
对象再由UIApplication对象调用其sendAction:to:fromSender:forEvent:方法来将消息分发到指定的target上
最终突破口 >>> UIControl完成按钮点击事件消息的包装与发送的阶段,可以做一些间隔时间处理点击消息发送
我们可以在UIControl的
sendAction:to:forEvent:
做防止按钮连续处理.
那么大概有如下几种做法:
第一种、自定义我们的UIButton类,以后程序中都使用我们UIButton类(只适合新项目,不太适合老项目,用的地方太多了)
第二种、使用UIButton Category封装防止按钮连续点击处理的逻辑(这种挺好,对原来的UIButton使用代码绿色无公害)
第三站、直接在main.m中执行main()之前,就替换掉UIControl的
sendAction:to:forEvent:
具体实现(稍微有点复杂)
首先看下使用UIButton子类实现
#import <UIKit/UIKit.h> @interface MyButton : UIButton /**
* 按钮点击的间隔时间
*/
@property (nonatomic, assign) NSTimeInterval time; @end
#import "MyButton.h" // 默认的按钮点击时间
static const NSTimeInterval defaultDuration = 3.0f; // 记录是否忽略按钮点击事件,默认第一次执行事件
static BOOL _isIgnoreEvent = NO; // 设置执行按钮事件状态
static void resetState() {
_isIgnoreEvent = NO;
} @implementation MyButton - (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event { //1. 按钮点击间隔事件
_time = _time == ? defaultDuration : _time; //2. 是否忽略按钮点击事件
if (_isIgnoreEvent) {
//2.1 忽略按钮事件 // 直接拦截掉super函数进行发送消息
return; } else if(_time > ) {
//2.2 不忽略按钮事件 // 后续在间隔时间内直接忽略按钮事件
_isIgnoreEvent = YES; // 间隔事件后,执行按钮事件
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_time * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
resetState();
}); // 发送按钮点击消息
[super sendAction:action to:target forEvent:event];
}
} @end
ViewController中测试
@implementation ViewController - (void)btnDidClick:(id)sender {
NSLog(@"我被点击了 >>> %@", NSStringFromSelector(_cmd));
} - (void)viewDidLoad {
[super viewDidLoad]; MyButton *btn = [[MyButton alloc] init];
[btn setTitle:@"点我啊" forState:UIControlStateNormal];
[btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
btn.layer.borderWidth = ;
btn.frame = CGRectMake(, , , );
[self.view addSubview:btn]; // 设置按钮的点击间隔时间
btn.time = .f; [btn addTarget:self action:@selector(btnDidClick:) forControlEvents:UIControlEventTouchUpInside];
}
运行程序后狂点按钮后的log如下
-- ::39.998 RuntimeDemo[:] 我被点击了 >>> btnDidClick:
-- ::42.308 RuntimeDemo[:] 我被点击了 >>> btnDidClick:
-- ::44.545 RuntimeDemo[:] 我被点击了 >>> btnDidClick:
-- ::46.783 RuntimeDemo[:] 我被点击了 >>> btnDidClick:
-- ::49.046 RuntimeDemo[:] 我被点击了 >>> btnDidClick:
-- ::51.281 RuntimeDemo[:] 我被点击了 >>> btnDidClick:
-- ::53.526 RuntimeDemo[:] 我被点击了 >>> btnDidClick:
-- ::55.886 RuntimeDemo[:] 我被点击了 >>> btnDidClick:
可以看到点击间隔最小是2秒
使用UIButton Category封装防止按钮连续点击的具体实现
其实大体上逻辑和上面的实现差不多,只是因为在Category分类里面,无法完成
重写sendAction:to:forEvent:
对应的实现,只能通过运行时替换掉sendAction:to:forEvent:具体实现
之后拦截到UIButton的sendAction:to:forEvent:方式执行时,将上面例子的逻辑加进来.
- UIButton分类完成按钮防止连续点击的代码实现
#import <UIKit/UIKit.h> @interface UIButton (Helper) /**
* 按钮点击的间隔时间
*/
@property (nonatomic, assign) NSTimeInterval clickDurationTime; @end
#import "UIButton+Helper.h"
#import <objc/runtime.h> // 默认的按钮点击时间
static const NSTimeInterval defaultDuration = 3.0f; // 记录是否忽略按钮点击事件,默认第一次执行事件
static BOOL _isIgnoreEvent = NO; // 设置执行按钮事件状态
static void resetState() {
_isIgnoreEvent = NO;
} @implementation UIButton (Helper) @dynamic clickDurationTime; + (void)load {
SEL originSEL = @selector(sendAction:to:forEvent:);
SEL mySEL = @selector(my_sendAction:to:forEvent:); Method originM = class_getInstanceMethod([self class], originSEL);
const char *typeEncodinds = method_getTypeEncoding(originM); Method newM = class_getInstanceMethod([self class], mySEL);
IMP newIMP = method_getImplementation(newM); if (class_addMethod([self class], mySEL, newIMP, typeEncodinds)) {
class_replaceMethod([self class], originSEL, newIMP, typeEncodinds);
} else {
method_exchangeImplementations(originM, newM);
}
} - (void)my_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event { // 保险起见,判断下Class类型
if ([self isKindOfClass:[UIButton class]]) { //1. 按钮点击间隔事件
self.clickDurationTime = self.clickDurationTime == ? defaultDuration : self.clickDurationTime; //2. 是否忽略按钮点击事件
if (_isIgnoreEvent) {
//2.1 忽略按钮事件
return;
} else if(self.clickDurationTime > ) {
//2.2 不忽略按钮事件 // 后续在间隔时间内直接忽略按钮事件
_isIgnoreEvent = YES; // 间隔事件后,执行按钮事件
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.clickDurationTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
resetState();
}); // 发送按钮点击消息
[self my_sendAction:action to:target forEvent:event];
} } else {
[self my_sendAction:action to:target forEvent:event];
}
} #pragma mark - associate - (void)setClickDurationTime:(NSTimeInterval)clickDurationTime {
objc_setAssociatedObject(self, @selector(clickDurationTime), @(clickDurationTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
} - (NSTimeInterval)clickDurationTime {
return [objc_getAssociatedObject(self, @selector(clickDurationTime)) doubleValue];
} @end
对作者的代码稍微做了一些修改,将一些不必要的oc函数直接写成c函数、c全局变量.
- 使用分类的UIButton类
#import <UIKit/UIKit.h> //导入分类即可
#import "UIButton+Helper.h" @interface MyButton : UIButton @end
#import "MyButton.h" @implementation MyButton @end
我们的按钮类不需要做任何的事情,完全不知道被拦截附加完成了防止连续点击的逻辑.
- 最后ViewController测试类
基本上不需要做什么修改,可以导入UIButton分类,对该按钮设置点击间隔时间.
OK,这个问题就到此为止了解决了,以及整个分析的过程记录完毕.
学习来源
http://www.cocoachina.com/ios/20160111/14932.html
原文: http://xiongzenghuidegithub.github.io/blog/2016/04/22/runtimeying-yong-fang-zhi-an-niu-lian-xu-dian-ji/
Runtime应用防止按钮连续点击 (转)的更多相关文章
- Android防止按钮连续点击
为了防止用户或者测试MM疯狂的点击某个button,写个方法防止按钮连续点击. public class Utils { private static long lastClickTime; publ ...
- Android 防止按钮连续点击的方法(Button,ImageButton等)
防止按钮连续点击 其实实现很简单 共通方法 public class Utils { private static long lastClickTime; public static boolean ...
- Android通过AOP实现防止按钮连续点击
防止连续点击的实现方式有很多种,比如,在所有的onclick里面加上防多次点击的代码,或者定义一个新的OnClickListener,在里面加上防多次点击的代码,然后项目中的所有OnClickList ...
- android防止按钮连续点击方案之AOP
转载请标明出处http://www.cnblogs.com/yxx123/p/6675567.html 防止连续点击的实现方式有很多种,比如,在所有的onclick里面加上防多次点击的代码,或者定义一 ...
- iOS 用RunTime来提升按钮的体验
用RunTime来提升按钮的体验 载请标明出处:http://blog.csdn.net/sk719887916/article/details/52597388,作者:Ryan 经常处理按钮问题都是 ...
- vue项目引入FastClick组件解决IOS系统下h5页面中的按钮点击延迟,连续点击无反应的问题
异常描述: ios系统手机中访问h5页面,按钮点击有延迟,连续点击卡顿.无反应. 异常原因: 这要追溯至 2007 年初.苹果公司在发布首款 iPhone 前夕,遇到一个问题:当时的网站都是为大屏幕设 ...
- WinForm连续点击按钮只打开一次窗体
许多朋友,学习C#时,制作WinForm小程序总会有一个问题,如果我们在父窗体设置的是点击一个按钮,打开一个子窗体,连续点击总会连续出现一样窗体,可是我们有时只想打开一次窗体,怎么办? 呵呵,我来方法 ...
- 防止表单submit或按钮button多次连续点击提交
如上例子:当我点击提交按钮触发submitQuartz()函数 防止用户连续点击提交操作 方法一:获取当时点击时间,根据时间差判断 $scope.submitQuartz=function () { ...
- 小程序连续点击bug解决
问题描述: 1)wxml片段 <view bindtap="loadMulti"> <text>连续点击,加载多次</text> </vi ...
随机推荐
- 数据库优化案例——————某市中心医院HIS系统
记得在自己学习数据库知识的时候特别喜欢看案例,因为优化的手段是容易掌握的,但是整体的优化思想是很难学会的.这也是为什么自己特别喜欢看案例,今天也开始分享自己做的优化案例. 最近一直很忙,博客产出也少的 ...
- Web性能优化:What? Why? How?
为什么要提升web性能? Web性能黄金准则:只有10%~20%的最终用户响应时间花在了下载html文档上,其余的80%~90%时间花在了下载页面组件上. web性能对于用户体验有及其重要的影响,根据 ...
- 关于全局ID,雪花(snowflake)算法的说明
上次简单的说一下:http://www.cnblogs.com/dunitian/p/6041745.html#uid C#版本的国外朋友已经封装了,大家可以去看看:https://github.co ...
- 探索ASP.NET MVC5系列之~~~4.模型篇---包含模型常用特性和过度提交防御
其实任何资料里面的任何知识点都无所谓,都是不重要的,重要的是学习方法,自行摸索的过程(不妥之处欢迎指正) 汇总:http://www.cnblogs.com/dunitian/p/4822808.ht ...
- Matlab slice方法和包络法绘制三维立体图
前言:在地球物理勘探,流体空间分布等多种场景中,定位空间点P(x,y,x)的物理属性值Q,并绘制三维空间分布图,对我们洞察空间场景有十分重要的意义. 1. 三维立体图的基本要件: 全空间网格化 网格节 ...
- 23种设计模式--中介者模式-Mediator Pattern
一.中介者模式的介绍 中介者模式第一下想到的就是中介,房子中介,婚姻中介啊等等,当然笔者也希望来个婚姻中介给我介绍一个哈哈哈,,回归正题中介者模式分成中介者类和用户类,根据接口编程的方式我们再 ...
- javascript排序
利用array中的sort()排序 w3cfunction sortNumber(a,b) { return a - b } var arr = new Array(6) arr[0] = " ...
- iOS之App Store上架被拒Legal - 5.1.5问题
今天在看到App Store 上架过程中,苹果公司反馈的拒绝原因发现了这么一个问题: Legal - 5.1.5 Your app uses background location services ...
- 让你从零开始学会写爬虫的5个教程(Python)
写爬虫总是非常吸引IT学习者,毕竟光听起来就很酷炫极客,我也知道很多人学完基础知识之后,第一个项目开发就是自己写一个爬虫玩玩. 其实懂了之后,写个爬虫脚本是很简单的,但是对于新手来说却并不是那么容易. ...
- 【一起学OpenFoam】01 OpenFoam的优势
CFD技术发展到今天,已经超过了大半个世纪了,已经涌现出非常多的CFD软件可供人们使用.通用商业CFD软件譬如Fluent.CFX.Star CCM+等在工业上得到了广泛的应用,另外一些专用的软件(如 ...