1.需求
曾经有一个app摆在我的面前,然后我对每个按钮进行疯狂连续点击,结果出现了不可描述的BUG,其实这个app就是我们自己开发的,所以修复这个BUG迫在眉睫.
2.原理
在button的响应方法里断个点,查看调用栈每次都有走过这个方法:
- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event
所以基本思路就是,能够在每次sendAction的时候都判断一下时间戳,如果和上次send的时间戳相隔过短,则终止send,否则放开让它继续send.
但是涉及到一个核心问题:
- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event
是系统方法,我改不了.
解决这个问题有两种思路:
(1)继承UIButton,定义一个CustomButton类,在这个子类里重写sendAction...
方法.
(2) 使用运行时函数,替换掉UIButton的sendAction...
方法.
方式(1)要改的地方太多了,几乎每个Button都要搜一下,替换一下,还有xib里的...不合适,因此采用方式(2).
2.实现
讲方法替换的文章网上太多,就不再复制粘贴。
直接贴代码,和注释
UIButton+InsensitiveTouch.h
#import <UIKit/UIKit.h>
@interface UIButton (InsensitiveTouch)
//开启UIButton防连点模式
+ (void)enableInsensitiveTouch;
//关闭UIButton防连点模式
+ (void)disableInsensitiveTouch;
//设置防连续点击最小时间差(s),不设置则默认值是0.5s
+ (void)setInsensitiveMinTimeInterval:(NSTimeInterval)interval;
@end
UIButton+InsensitiveTouch.m
#import "UIButton+InsensitiveTouch.h"
#import <objc/runtime.h>
//最小时间差
static NSTimeInterval insensitiveMinTimeInterval = 0.5;
//原生sendAction:to:forEvent:实现
static void (*originalImplementation)(id, SEL, SEL, id, UIEvent *) = NULL;
//替换的sendAction:to:forEvent:实现
static void replacedImplementation(id object, SEL selector, SEL action, id target, UIEvent *event);
@implementation UIButton (InsensitiveTouch)
+ (void)enableInsensitiveTouch {
//获取当前"@selector(sendAction:to:forEvent:)"对应的Method
Method methodNow = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
//得到当前sendAction:to:forEvent:实现地址
IMP implementationNow = method_getImplementation(methodNow);
//这个实现地址已经是replacedImplementation,说明已经替换过了
if (implementationNow == (IMP)replacedImplementation) {
return;
}
//保存原生的sendAction:to:forEvent:实现地址
originalImplementation = (void (*)(id, SEL, SEL, id, UIEvent *))implementationNow;
const char *type = method_getTypeEncoding(methodNow);
//将实现替换为replacedImplementation
class_replaceMethod(self, @selector(sendAction:to:forEvent:), (IMP)replacedImplementation, type);
}
+ (void)disableInsensitiveTouch {
IMP implementationNow = class_getMethodImplementation(self, @selector(sendAction:to:forEvent:));
if (originalImplementation && implementationNow == (IMP)replacedImplementation) {
class_replaceMethod(self, @selector(sendAction:to:forEvent:), (IMP)originalImplementation, NULL);
}
}
+ (void)setInsensitiveMinTimeInterval:(NSTimeInterval)interval {
insensitiveMinTimeInterval = interval;
}
- (NSTimeInterval)lastTouchTimestamp {
return [objc_getAssociatedObject(self, @selector(lastTouchTimestamp)) doubleValue];
}
- (void)setLastTouchTimestamp:(NSTimeInterval)timestamp {
objc_setAssociatedObject(self, @selector(lastTouchTimestamp), @(timestamp), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
//替换的sendAction:to:forEvent:实现
static void replacedImplementation(id object, SEL selector, SEL action, id target, UIEvent *event) {
//是按钮,并且是UIEventTypeTouches事件,才进行时间戳判断
//但是要排除这两种按钮 “CUShutterButton”和 "CAMShutterButton",这两个分别是8系统,10系统上相机拍照按钮的类名.这是两个特殊封装过的按钮,如果把它们的事件也用时间戳给过滤掉了,你就会发现app里弹出相机后,要长按才能拍照。
if ([object isKindOfClass:UIButton.self] && ![NSStringFromClass([object class]) isEqualToString:@"CUShutterButton"] && ![NSStringFromClass([object class]) isEqualToString:@"CAMShutterButton"] && event.type == UIEventTypeTouches) {
//进行时间戳判断
UIButton *button = (UIButton *)object;
if (ABS(event.timestamp - button.lastTouchTimestamp) < insensitiveMinTimeInterval) {
//时间过短,就此返回,此次事件Send也中止
return;
}
button.lastTouchTimestamp = event.timestamp;
}
//时间戳上没问题,不属于快速点击
if (originalImplementation) {
//调用系统原生实现,继续完成事件的Send
originalImplementation(object, selector, action, target, event);
}
}
3.使用
在Appdelegate launchWith...
里调用 [UIButton enableInsensitiveTouch]
即可。网上大部分实现喜欢在+(void)load方法里完成替换,因此使用库的时候什么都不用调用,但我还是觉得让使用者知道自己做了什么比较好。