好久之前就看到过使用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:
  • 当最后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应用防止按钮连续点击 (转)的更多相关文章

  1. Android防止按钮连续点击

    为了防止用户或者测试MM疯狂的点击某个button,写个方法防止按钮连续点击. public class Utils { private static long lastClickTime; publ ...

  2. Android 防止按钮连续点击的方法(Button,ImageButton等)

    防止按钮连续点击  其实实现很简单 共通方法 public class Utils { private static long lastClickTime; public static boolean ...

  3. Android通过AOP实现防止按钮连续点击

    防止连续点击的实现方式有很多种,比如,在所有的onclick里面加上防多次点击的代码,或者定义一个新的OnClickListener,在里面加上防多次点击的代码,然后项目中的所有OnClickList ...

  4. android防止按钮连续点击方案之AOP

    转载请标明出处http://www.cnblogs.com/yxx123/p/6675567.html 防止连续点击的实现方式有很多种,比如,在所有的onclick里面加上防多次点击的代码,或者定义一 ...

  5. iOS 用RunTime来提升按钮的体验

    用RunTime来提升按钮的体验 载请标明出处:http://blog.csdn.net/sk719887916/article/details/52597388,作者:Ryan 经常处理按钮问题都是 ...

  6. vue项目引入FastClick组件解决IOS系统下h5页面中的按钮点击延迟,连续点击无反应的问题

    异常描述: ios系统手机中访问h5页面,按钮点击有延迟,连续点击卡顿.无反应. 异常原因: 这要追溯至 2007 年初.苹果公司在发布首款 iPhone 前夕,遇到一个问题:当时的网站都是为大屏幕设 ...

  7. WinForm连续点击按钮只打开一次窗体

    许多朋友,学习C#时,制作WinForm小程序总会有一个问题,如果我们在父窗体设置的是点击一个按钮,打开一个子窗体,连续点击总会连续出现一样窗体,可是我们有时只想打开一次窗体,怎么办? 呵呵,我来方法 ...

  8. 防止表单submit或按钮button多次连续点击提交

    如上例子:当我点击提交按钮触发submitQuartz()函数 防止用户连续点击提交操作 方法一:获取当时点击时间,根据时间差判断 $scope.submitQuartz=function () { ...

  9. 小程序连续点击bug解决

    问题描述: 1)wxml片段 <view bindtap="loadMulti"> <text>连续点击,加载多次</text> </vi ...

随机推荐

  1. Golang, 以17个简短代码片段,切底弄懂 channel 基础

    (原创出处为本博客:http://www.cnblogs.com/linguanh/) 前序: 因为打算自己搞个基于Golang的IM服务器,所以复习了下之前一直没怎么使用的协程.管道等高并发编程知识 ...

  2. URL安全的Base64编码

    Base64编码可用于在HTTP环境下传递较长的标识信息.在其他应用程序中,也常常需要把二进制数据编码为适合放在URL(包括隐藏表单域)中的形式.此时,采用Base64编码不仅比较简短,同时也具有不可 ...

  3. 在ASP.NET Core中使用百度在线编辑器UEditor

    在ASP.NET Core中使用百度在线编辑器UEditor 0x00 起因 最近需要一个在线编辑器,之前听人说过百度的UEditor不错,去官网下了一个.不过服务端只有ASP.NET版的,如果是为了 ...

  4. 漫扯:从polling到Websocket

    Http被设计成了一个单向的通信的协议,即客户端发起一个request,然后服务器回应一个response.这让服务器很为恼火:我特么才是老大,我居然不能给小弟发消息... 轮询 老大发火了,小弟们自 ...

  5. 预览github里面的网页或dome

    1.问题所在: 之前把项目提交到github都可以在路径前面加上http://htmlpreview.github.io/?来预览demo,最近发现这种方式预览的时候加载不出来css,js(原因不详) ...

  6. Linux主机上使用交叉编译移植u-boot到树莓派

    0环境 Linux主机OS:Ubuntu14.04 64位,运行在wmware workstation 10虚拟机 树莓派版本:raspberry pi 2 B型. 树莓派OS: Debian Jes ...

  7. Struts的拦截器

    Struts的拦截器 1.什么是拦截器 Struts的拦截器和Servlet过滤器类似,在执行Action的execute方法之前,Struts会首先执行Struts.xml中引用的拦截器,在执行完所 ...

  8. 札记:Java异常处理

    异常概述 程序在运行中总会面临一些"意外"情况,良好的代码需要对它们进行预防和处理.大致来说,这些意外情况分三类: 交互输入 用户以非预期的方式使用程序,比如非法输入,不正当的操作 ...

  9. Visual Studio 2015正式发布

    Windows 10 RTM正式版要7月29日发布,微软的另一个重磅软件Visual Studio 2015已经率先发布,今天如期放出了正式版本.Visual Studio 2015包括许多新功能和更 ...

  10. Hadoop2 自己动手编译Hadoop的eclipse插件

    前言:       毕业两年了,之前的工作一直没有接触过大数据的东西,对hadoop等比较陌生,所以最近开始学习了.对于我这样第一次学的人,过程还是充满了很多疑惑和不解的,不过我采取的策略是还是先让环 ...