Runtime应用防止按钮连续点击 (转)

好久之前就看到过使用Runtime解决按钮的连续点击的问题,一直觉得没啥好记录的。刚好今天旁边同时碰到这个问题,看他们好捉急而且好像很难处理,于是我先自己看看…

前面自己也学习了很多Runtime的东西,一直觉得这个按钮连续点击其实很简单,就使用Runtime交换SEL实现IMP即可,但其实没明白解决这个问题的过程.

虽然直接可以在github搜到解决方法,但是还是有必要学习一下解决这个问题的一步一步的思路,给出这个作者的git:

1 https://github.com/strivever/UIButton-touch
 1 @implementation ViewController
 2
 3 - (void)btnDidClick:(id)sender {
 4     NSLog(@"我被点击了....");
 5 }
 6
 7 - (void)viewDidLoad {
 8     [super viewDidLoad];
 9
10     MyButton *btn = [[MyButton alloc] init];
11     [btn setTitle:@"点我啊" forState:UIControlStateNormal];
12     [btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
13     btn.layer.borderWidth = 1;
14     btn.frame = CGRectMake(50, 100, 100, 50);
15     [self.view addSubview:btn];
16
17     [btn addTarget:self action:@selector(btnDidClick:) forControlEvents:UIControlEventTouchUpInside];
18 }

如上代码是最简单的UIBUtton使用代码,但是有一个问题就是,按钮可以无限制、没有间隔时间、连续的n次点击都会触发处理函数.


iOS中的按钮事件机制 >>> Target-Action机制

  • 用户点击时,产生一个按钮点击事件消息
  • 这个消息发送给注册的Target处理
  • Target接收到消息,然后查找自己的SEL对应的具体实现IMP正儿八经的去处理点击事件

实际上该点击消息包含三个东西:

  • Target处理者
  • SEL方法Id
  • 按钮事件当时触发时的状态
 1 所有的按钮事件状态
 2
 3 typedef NS_OPTIONS(NSUInteger, UIControlState) {
 4     UIControlStateNormal       = 0,
 5     UIControlStateHighlighted  = 1 << 0,                  // used when UIControl isHighlighted is set
 6     UIControlStateDisabled     = 1 << 1,
 7     UIControlStateSelected     = 1 << 2,                  // flag usable by app (see below)
 8     UIControlStateFocused NS_ENUM_AVAILABLE_IOS(9_0) = 1 << 3, // Applicable only when the screen supports focus
 9     UIControlStateApplication  = 0x00FF0000,              // additional flags available for application use
10     UIControlStateReserved     = 0xFF000000               // flags reserved for internal framework use
11 };

已经知道点击按钮时候,会产生一个包装了Target、SEL、按钮事件状态三个东西的消息发送给Target处理

问题: 是谁来包装UIButton的点击事件消息,并且完成发送消息了?

这个是解决连续点击按钮的关键问题所在,必须搞清楚。因为如果搞清楚具体包装和发送按钮点击时间消息的地方和时机,那么可以拦截这个地方执行,然后加入是否在指定的间隔时间内决定是否让其继续执行发送消息的操作。

那么问题不就解决了吗,我都不让他发送消息了,他还能执行?


首先从UIButton.h头文件中查找,是否有send message 、send Action …等等包含send的方法

无法找到.


UIButton继承自UIControl,而UIControl又负责很多的UI事件处理,那么可以继续从UIControl.h中查找

找到两个send相关的函数:

1 // 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.
2 - (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;
1 - (void)sendActionsForControlEvents:(UIControlEvents)controlEvents;                        // send all actions associated with events

没看懂注释有什么意思,那么代码直接试把.

前面我用自己的UIButton子类是有原因的,可以重写父类方法完成父类方法Hook的效果.

尝试进行hook UIControl的 sendAction:to: forEvent:

1 #import <UIKit/UIKit.h>
2
3 @interface MyButton : UIButton
4
5 @end
 1 #import "MyButton.h"
 2
 3 @implementation MyButton
 4
 5 - (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
 6
 7     NSLog(@"Pre sendAction >>>> action = %@", NSStringFromSelector(action));
 8
 9     [super sendAction:action to:target forEvent:event];
10
11     NSLog(@"After sendAction >>>> action = %@", NSStringFromSelector(action));
12 }
13
14 @end

然后在ViewController中也进行下修改,确定按钮响应函数与这个sendAction:to: forEvent:执行的顺序.

 1 @implementation ViewController
 2
 3 - (void)btnDidClick:(id)sender {
 4     NSLog(@"我被点击了 >>> %@", NSStringFromSelector(_cmd));
 5 }
 6
 7 - (void)viewDidLoad {
 8     [super viewDidLoad];
 9
10     MyButton *btn = [[MyButton alloc] init];
11     [btn setTitle:@"点我啊" forState:UIControlStateNormal];
12     [btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
13     btn.layer.borderWidth = 1;
14     btn.frame = CGRectMake(50, 100, 100, 50);
15     [self.view addSubview:btn];
16
17     [btn addTarget:self action:@selector(btnDidClick:) forControlEvents:UIControlEventTouchUpInside];
18 }

最后输出结果如下

1 2016-04-22 15:43:14.181 RuntimeDemo[35850:366849] Pre sendAction >>>> action = btnDidClick:
2 2016-04-22 15:43:14.183 RuntimeDemo[35850:366849] 我被点击了 >>> btnDidClick:
3 2016-04-22 15:43:14.183 RuntimeDemo[35850:366849] 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子类实现

 1 #import <UIKit/UIKit.h>
 2
 3 @interface MyButton : UIButton
 4
 5 /**
 6  *  按钮点击的间隔时间
 7  */
 8 @property (nonatomic, assign) NSTimeInterval time;
 9
10 @end
 1 #import "MyButton.h"
 2
 3 // 默认的按钮点击时间
 4 static const NSTimeInterval defaultDuration = 3.0f;
 5
 6 // 记录是否忽略按钮点击事件,默认第一次执行事件
 7 static BOOL _isIgnoreEvent = NO;
 8
 9 // 设置执行按钮事件状态
10 static void resetState() {
11     _isIgnoreEvent = NO;
12 }
13
14 @implementation MyButton
15
16 - (void)sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
17
18     //1. 按钮点击间隔事件
19     _time = _time == 0 ? defaultDuration : _time;
20
21     //2. 是否忽略按钮点击事件
22     if (_isIgnoreEvent) {
23         //2.1 忽略按钮事件
24
25         // 直接拦截掉super函数进行发送消息
26         return;
27
28     } else if(_time > 0) {
29         //2.2 不忽略按钮事件
30
31         // 后续在间隔时间内直接忽略按钮事件
32         _isIgnoreEvent = YES;
33
34         // 间隔事件后,执行按钮事件
35         dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_time * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
36             resetState();
37         });
38
39         // 发送按钮点击消息
40         [super sendAction:action to:target forEvent:event];
41     }
42 }
43
44 @end

ViewController中测试

 1 @implementation ViewController
 2
 3 - (void)btnDidClick:(id)sender {
 4     NSLog(@"我被点击了 >>> %@", NSStringFromSelector(_cmd));
 5 }
 6
 7 - (void)viewDidLoad {
 8     [super viewDidLoad];
 9
10   MyButton *btn = [[MyButton alloc] init];
11     [btn setTitle:@"点我啊" forState:UIControlStateNormal];
12     [btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
13     btn.layer.borderWidth = 1;
14     btn.frame = CGRectMake(50, 100, 100, 50);
15     [self.view addSubview:btn];
16
17     // 设置按钮的点击间隔时间
18     btn.time = 2.f;
19
20     [btn addTarget:self action:@selector(btnDidClick:) forControlEvents:UIControlEventTouchUpInside];
21 }

运行程序后狂点按钮后的log如下

1 2016-04-22 16:58:39.998 RuntimeDemo[40146:474695] 我被点击了 >>> btnDidClick:
2 2016-04-22 16:58:42.308 RuntimeDemo[40146:474695] 我被点击了 >>> btnDidClick:
3 2016-04-22 16:58:44.545 RuntimeDemo[40146:474695] 我被点击了 >>> btnDidClick:
4 2016-04-22 16:58:46.783 RuntimeDemo[40146:474695] 我被点击了 >>> btnDidClick:
5 2016-04-22 16:58:49.046 RuntimeDemo[40146:474695] 我被点击了 >>> btnDidClick:
6 2016-04-22 16:58:51.281 RuntimeDemo[40146:474695] 我被点击了 >>> btnDidClick:
7 2016-04-22 16:58:53.526 RuntimeDemo[40146:474695] 我被点击了 >>> btnDidClick:
8 2016-04-22 16:58:55.886 RuntimeDemo[40146:474695] 我被点击了 >>> btnDidClick:

可以看到点击间隔最小是2秒


使用UIButton Category封装防止按钮连续点击的具体实现

其实大体上逻辑和上面的实现差不多,只是因为在Category分类里面,无法完成重写sendAction:to:forEvent:对应的实现,只能通过运行时替换掉sendAction:to:forEvent:具体实现之后拦截到UIButton的sendAction:to:forEvent:方式执行时,将上面例子的逻辑加进来.

  • UIButton分类完成按钮防止连续点击的代码实现
 1 #import <UIKit/UIKit.h>
 2
 3 @interface UIButton (Helper)
 4
 5 /**
 6  *  按钮点击的间隔时间
 7  */
 8 @property (nonatomic, assign) NSTimeInterval clickDurationTime;
 9
10 @end
 1 #import "UIButton+Helper.h"
 2 #import <objc/runtime.h>
 3
 4 // 默认的按钮点击时间
 5 static const NSTimeInterval defaultDuration = 3.0f;
 6
 7 // 记录是否忽略按钮点击事件,默认第一次执行事件
 8 static BOOL _isIgnoreEvent = NO;
 9
10 // 设置执行按钮事件状态
11 static void resetState() {
12     _isIgnoreEvent = NO;
13 }
14
15 @implementation UIButton (Helper)
16
17 @dynamic clickDurationTime;
18
19 + (void)load {
20     SEL originSEL = @selector(sendAction:to:forEvent:);
21     SEL mySEL = @selector(my_sendAction:to:forEvent:);
22
23     Method originM = class_getInstanceMethod([self class], originSEL);
24     const char *typeEncodinds = method_getTypeEncoding(originM);
25
26     Method newM = class_getInstanceMethod([self class], mySEL);
27     IMP newIMP = method_getImplementation(newM);
28
29     if (class_addMethod([self class], mySEL, newIMP, typeEncodinds)) {
30         class_replaceMethod([self class], originSEL, newIMP, typeEncodinds);
31     } else {
32         method_exchangeImplementations(originM, newM);
33     }
34 }
35
36 - (void)my_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
37
38     // 保险起见,判断下Class类型
39     if ([self isKindOfClass:[UIButton class]]) {
40
41         //1. 按钮点击间隔事件
42         self.clickDurationTime = self.clickDurationTime == 0 ? defaultDuration : self.clickDurationTime;
43
44         //2. 是否忽略按钮点击事件
45         if (_isIgnoreEvent) {
46             //2.1 忽略按钮事件
47             return;
48         } else if(self.clickDurationTime > 0) {
49             //2.2 不忽略按钮事件
50
51             // 后续在间隔时间内直接忽略按钮事件
52             _isIgnoreEvent = YES;
53
54             // 间隔事件后,执行按钮事件
55             dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.clickDurationTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
56                 resetState();
57             });
58
59             // 发送按钮点击消息
60             [self my_sendAction:action to:target forEvent:event];
61         }
62
63     } else {
64         [self my_sendAction:action to:target forEvent:event];
65     }
66 }
67
68 #pragma mark - associate
69
70 - (void)setClickDurationTime:(NSTimeInterval)clickDurationTime {
71     objc_setAssociatedObject(self, @selector(clickDurationTime), @(clickDurationTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
72 }
73
74 - (NSTimeInterval)clickDurationTime {
75     return [objc_getAssociatedObject(self, @selector(clickDurationTime)) doubleValue];
76 }
77
78 @end

对作者的代码稍微做了一些修改,将一些不必要的oc函数直接写成c函数、c全局变量.

  • 使用分类的UIButton类
1 #import <UIKit/UIKit.h>
2
3 //导入分类即可
4 #import "UIButton+Helper.h"
5
6 @interface MyButton : UIButton
7
8 @end
1 #import "MyButton.h"
2
3 @implementation MyButton
4
5 @end

我们的按钮类不需要做任何的事情,完全不知道被拦截附加完成了防止连续点击的逻辑.

  • 最后ViewController测试类

基本上不需要做什么修改,可以导入UIButton分类,对该按钮设置点击间隔时间.

OK,这个问题就到此为止了解决了,以及整个分析的过程记录完毕.

学习来源

1 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/

时间: 2024-10-03 13:46:21

Runtime应用防止按钮连续点击 (转)的相关文章

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

防止按钮连续点击 共通方法: public class Utils { private static long lastClickTime; public static boolean isFastDoubleClick() { long time = System.currentTimeMillis(); if ( time - lastClickTime < 500) { return true; } lastClickTime = time; return false; } } 按钮点击时

Android防止按钮连续点击

为了防止用户或者测试MM疯狂的点击某个button,写个方法防止按钮连续点击. public class Utils { private static long lastClickTime; public synchronized static boolean isFastClick() { long time = System.currentTimeMillis(); if ( time - lastClickTime < 500) { return true; } lastClickTime

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

许多朋友,学习C#时,制作WinForm小程序总会有一个问题,如果我们在父窗体设置的是点击一个按钮,打开一个子窗体,连续点击总会连续出现一样窗体,可是我们有时只想打开一次窗体,怎么办? 呵呵,我来方法告诉大家,希望对大家有所帮助,当然,我理解初学者的心情,能看懂,保证代码可用,希望对你们有帮助.   //设置只打开一次公用方法 public static int cxypx(Form frmMdiFather, string strMdiChild) { int bReturn = -1; fo

iOS小技巧--用runtime 解决UIButton 重复点击问题

iOS小技巧–用runtime 解决UIButton 重复点击问题 什么是这个问题 我们的按钮是点击一次响应一次, 即使频繁的点击也不会出问题, 可是某些场景下还偏偏就是会出问题. 通常是如何解决 我们通常会在按钮点击的时候设置这个按钮不可点击. 等待0.xS的延时后,在设置回来; 或者在操作结束的时候设置可以点击. - (IBAction)clickBtn1:(UIbutton *)sender { sender.enabled = NO; doSomething sender.enabled

iOS-UIButton防止连续点击(点击抖动)

UIButton是我们iOS开发中常用的控件,连续/抖动点击也是用户使用中常发生的 !项目之后发现网上解决这一体验问题的资料还是蛮多的,但还是要自己做份笔记,方便下次查阅! 方案一: - (void)viewDidLoad{ [super viewDidLoad]; // 1. 创建 btn 并添加点击事件 UIButton *btn = [UIButton buttonWithType:UIButtonTypeCustom]; [btn setFrame:CGRectMake(100, 100

wpf,后台触发按钮点击以及拖动

触发按钮Click MouseButtonEventArgs args = new MouseButtonEventArgs(Mouse.PrimaryDevice, 0, MouseButton.Left); args.RoutedEvent = Button.ClickEvent; btnOkCommand.RaiseEvent(args); 触发按钮绑定的Command 需要添加UIAutomationProvider 引用 ButtonAutomationPeer bam = new B

iOS小技巧:用runtime 解决UIButton 重复点击问题

http://www.cocoachina.com/ios/20150911/13260.html 作者:uxyheaven 授权本站转载. 什么是这个问题 我们的按钮是点击一次响应一次, 即使频繁的点击也不会出问题, 可是某些场景下还偏偏就是会出问题. 通常是如何解决 我们通常会在按钮点击的时候设置这个按钮不可点击. 等待0.xS的延时后,在设置回来; 或者在操作结束的时候设置可以点击. 1 2 3 4 5 6 - (IBAction)clickBtn1:(UIbutton *)sender

小程序连续点击bug解决

问题描述: 1)wxml片段 <view bindtap="loadMulti"> <text>连续点击,加载多次</text> </view> <view bindtap="loadOnce"> <text>连续点击,加载一次</text> </view> 2)js代码片段 loadMulti:function(e) { wx.navigateTo({ url: '/p

Android实现按钮点击效果(第一次点击变色,第二次恢复)

1.首先创建一个按钮 <Button android:id="@+id/click" android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="点击变色" android:background="@drawable/btn_st" android:gravity="center&