防止Button连续点击-Runtime的使用

面试时被问及的一个问题,容我技术菜接触少被问及时虽然吞吞吐吐说了点,但毕竟没接触也不是很有信心,具体怎么实现更不用说了。面试完回来后就翻阅各种文章发现用的最多的就是Runtime的方法交换功能了。在此只是个人的知识点整理,各位客官如有不足多多提点。

首选创建一个UIButton的分类

1
2
// .h中添加该属性 用于调用时设置延迟时间 这样不同的button可以设置不同的延迟时间
@property (nonatomic, assign) NSTimeInterval delay_interval;

既然添加了属性当然就要利用Runtime的添加属性功能为delay_interval设置对应的getter和setter方法。

1
2
3
4
5
6
7
- (NSTimeInterval)delay_interval {
return [objc_getAssociatedObject(self, delayintervalkey) doubleValue];
}

- (void)setDelay_interval:(NSTimeInterval)delay_interval {
objc_setAssociatedObject(self, delayintervalkey, @(delay_interval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

在.m中一个BOOL用来判断按钮的点击状态

1
2
3
4
5
@interface UIButton ()

@property (nonatomic, assign) BOOL clickBool;

@end

设置getter和setter

1
2
3
4
5
6
7
- (BOOL)clickBool {
return [objc_getAssociatedObject(self, unclickkey) doubleValue];
}

- (void)setClickBool:(BOOL)clickBool {
objc_setAssociatedObject(self, unclickkey, @(clickBool), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

设置相关Key

1
2
static char * const delayintervalkey = "delayintervalkey";
static char * const unclickkey = "unclickkey";

接下来就是方法交换的实现了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
+ (void)load {
Method frommethod = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
Method tomethod = class_getInstanceMethod(self, @selector(delayedClick:to:forEvent:));
method_exchangeImplementations(frommethod, tomethod);
}

- (void)delayedClick:(SEL)action to:(id)target forEvent:(UIEvent *)event {

if (self.clickBool == NO) {
self.clickBool = YES;
[self delayedClick:action to:target forEvent:event];
[self performSelector:@selector(setClickBool:) withObject:@(NO) afterDelay:self.delay_interval];
}
}

- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;方法的作用在于当交换方法完成且已被点击后重新设置按钮的状态以用来触发下一次点击。这个方法是单线程的,也就是说只有当前调用此方法的函数执行完毕后,selector方法才会被调用

最后就是调用了

1
2
// 设置延迟时间即可
clickButton.delay_interval = 5;

正常点击的时候发现并没有什么问题,然而当我PUSH后在点击返回按钮时竟然崩溃了。报错信息如下

1
2
3
4
5
6
7
2019-05-11 13:44:22.938692+0800 Test[1336:125411] -[_UIButtonBarButton clickBool]: unrecognized selector sent to instance 0x10084e6b0

2019-05-11 13:44:35.281315+0800 Test[1336:125411] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[_UIButtonBarButton clickBool]: unrecognized selector sent to instance 0x10084e6b0'

*** First throw call stack:
(0x1b7c48518 0x1b6e239f8 0x1b7b65278 0x1e4072ef8 0x1b7c4dd60 0x1b7c4f9fc 0x1001d19cc 0x1e3af2e18 0x1e3af2f80 0x1e3af1e84 0x1e3c6b3d8 0x1b7bd989c 0x1b7bd45c4 0x1b7bd4b40 0x1b7bd4354 0x1b9dd479c 0x1e4047b68 0x1001d1fac 0x1b769a8e0)
libc++abi.dylib: terminating with uncaught exception of type NSException

问题应该是_UIButtonBarButton找不到调用的方法。我的理解是我们只是在UIButton的分类下调换了父类UIControl- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;方法。而不是在父类下直接交换了方法,导致在其他类调用- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;方法时而找不到对应的方法从而崩溃。

  • 所以交换方法的分类改成UIControl而不应该是UIButton. 这样其他子类在调用时不会受影响。
  • 总结后发现这么一个需求就利用到了Runtime的多个功能点。不得不说Runtime在某些功能上的功能强大。也正好为最近的Runtime学习整理来一个很好的例子。