1.需求
曾經(jīng)有一個app擺在我的面前,然后我對每個按鈕進行瘋狂連續(xù)點擊,結(jié)果出現(xiàn)了不可描述的BUG,其實這個app就是我們自己開發(fā)的,所以修復(fù)這個BUG迫在眉睫.
2.原理
在button的響應(yīng)方法里斷個點,查看調(diào)用棧每次都有走過這個方法:
- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event
所以基本思路就是,能夠在每次sendAction的時候都判斷一下時間戳,如果和上次send的時間戳相隔過短,則終止send,否則放開讓它繼續(xù)send.
但是涉及到一個核心問題:
- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event是系統(tǒng)方法,我改不了.
解決這個問題有兩種思路:
(1)繼承UIButton,定義一個CustomButton類,在這個子類里重寫sendAction...方法.
(2) 使用運行時函數(shù),替換掉UIButton的sendAction...方法.
方式(1)要改的地方太多了,幾乎每個Button都要搜一下,替換一下,還有xib里的...不合適,因此采用方式(2).
2.實現(xiàn)
講方法替換的文章網(wǎng)上太多,就不再復(fù)制粘貼。
直接貼代碼,和注釋
UIButton+InsensitiveTouch.h
#import <UIKit/UIKit.h>
@interface UIButton (InsensitiveTouch)
//開啟UIButton防連點模式
+ (void)enableInsensitiveTouch;
//關(guān)閉UIButton防連點模式
+ (void)disableInsensitiveTouch;
//設(shè)置防連續(xù)點擊最小時間差(s),不設(shè)置則默認值是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:實現(xiàn)
static void (*originalImplementation)(id, SEL, SEL, id, UIEvent *) = NULL;
//替換的sendAction:to:forEvent:實現(xiàn)
static void replacedImplementation(id object, SEL selector, SEL action, id target, UIEvent *event);
@implementation UIButton (InsensitiveTouch)
+ (void)enableInsensitiveTouch {
//獲取當前"@selector(sendAction:to:forEvent:)"對應(yīng)的Method
Method methodNow = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
//得到當前sendAction:to:forEvent:實現(xiàn)地址
IMP implementationNow = method_getImplementation(methodNow);
//這個實現(xiàn)地址已經(jīng)是replacedImplementation,說明已經(jīng)替換過了
if (implementationNow == (IMP)replacedImplementation) {
return;
}
//保存原生的sendAction:to:forEvent:實現(xiàn)地址
originalImplementation = (void (*)(id, SEL, SEL, id, UIEvent *))implementationNow;
const char *type = method_getTypeEncoding(methodNow);
//將實現(xiàn)替換為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:實現(xiàn)
static void replacedImplementation(id object, SEL selector, SEL action, id target, UIEvent *event) {
//是按鈕,并且是UIEventTypeTouches事件,才進行時間戳判斷
//但是要排除這兩種按鈕 “CUShutterButton”和 "CAMShutterButton",這兩個分別是8系統(tǒng),10系統(tǒng)上相機拍照按鈕的類名.這是兩個特殊封裝過的按鈕,如果把它們的事件也用時間戳給過濾掉了,你就會發(fā)現(xiàn)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) {
//調(diào)用系統(tǒng)原生實現(xiàn),繼續(xù)完成事件的Send
originalImplementation(object, selector, action, target, event);
}
}
3.使用
在Appdelegate launchWith... 里調(diào)用 [UIButton enableInsensitiveTouch]即可。網(wǎng)上大部分實現(xiàn)喜歡在+(void)load方法里完成替換,因此使用庫的時候什么都不用調(diào)用,但我還是覺得讓使用者知道自己做了什么比較好。