iOS-通过Runtime防止重复点击-UIButton、UITableView
Gesture有系统处理单机双击,暂不去自定义时间间隔了。只处理UIButton、UITableView(UICollectionView)
1、思路:
UIButton hook sendAction
UITableView hook setDelegate(swift中没有此方法,改用OC)
Gesture hook initWithTarget:action:
Gesture hook initWithTarget:action:
注意:
1、hook button的sendAction方法其实hook的是UIControl的的sendAction,点击UIControl子类(导航栏返回按钮)会崩溃。因此,把UIButton hook换为UIControl hook即可,同时为了避免对其他UIControl子类(UISlider/UISwitch)造成影响和最小影响原则,默认关闭防止重复点击功能
[_UIButtonBarButton xx_sendActionWithAction:to:forEvent:]: unrecognized selector sent to instance 0x7f8e2a41b380'
2、OC中的方法是NSDate.date.timeIntervalSince1970,不是[[NSDate date] timeIntervalSince1970]
3、由于setDelegate方法可能被多次调用,所以要判断是否已经swizzling了,防止重复执行。基类A中hook了tableview之后,子类B、C分别setDelegate的话会调用两次method_exchange...(didSelectRowAtIndexPath)。具体表现为:基类为UITableViewController时无影响,基类为自定义的有UITableView的VC时,子类A正常,子类B异常。解决办法1:didSelectRow基类里面不写,子类里面自己去实现,或基类里面写了但子类各自去重写。解决方法2:更安全的运行时方法交换库 Aspects。
2、代码
+(void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalAppearSelector = @selector(setDelegate:);
SEL swizzingAppearSelector = @selector(my_setDelegate:);
method_exchangeImplementations(class_getInstanceMethod([self class], originalAppearSelector), class_getInstanceMethod([self class], swizzingAppearSelector));
});
}
-(void)my_setDelegate:(id<UITableViewDelegate>)delegate{
[self my_setDelegate:delegate];
SEL sel = @selector(tableView:didSelectRowAtIndexPath:);
SEL sel_t = @selector(my_tableView:didSelectRowAtIndexPath:);
//如果没实现tableView:didSelectRowAtIndexPath:就不需要hook
if (![delegate respondsToSelector:sel]){
return;
}
BOOL addsuccess = class_addMethod([delegate class],
sel_t,
method_getImplementation(class_getInstanceMethod([self class], sel_t)),
nil);
//如果添加成功了就直接交换实现, 如果没有添加成功,说明之前已经添加过并交换过实现了
if (addsuccess) {
Method selMethod = class_getInstanceMethod([delegate class], sel);
Method sel_Method = class_getInstanceMethod([delegate class], sel_t);
method_exchangeImplementations(selMethod, sel_Method);
}
}
// 由于我们交换了方法, 所以在tableview的 didselected 被调用的时候, 实质调用的是以下方法:
-(void)my_tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
if(NSDate.date.timeIntervalSince1970 - tableView.acceptEventTime < tableView.accpetEventInterval) {
NSLog(@"点击太快了");
return;
}
if (tableView.accpetEventInterval > 0) {
tableView.acceptEventTime = NSDate.date.timeIntervalSince1970;
}
[self my_tableView:tableView didSelectRowAtIndexPath:indexPath];
}
3、注意点、风险点
1、避免交换父类方法
如果当前类未实现被交换的方法而父类实现了的情况下,此时父类的实现会被交换,若此父类的多个继承者都在交换时会导致方法被交换多次而混乱,同时当调用父类的方法时会因为找不到而发生崩溃。所以在交换前都应该先尝试为当前类添加被交换的函数的新的实现IMP,如果添加成功则说明类没有实现被交换的方法,则只需要替代分类交换方法的实现为原方法的实现,如果添加失败,则原类中实现了被交换的方法,则可以直接进行交换。
2、load方法的加载顺序和相互影响
一个类B可能有继承来的super类A,还有可能有自己的分类C,如果分类中也实现了load方法,它们的调用顺序是怎么样的呢?系统首先会调用super的load方法,然后再调用类B自身的load方法,再次才会调用类B的分类C的load方法,也即是说真个继承链包括分类扩展中的load方法都会被执行到,只是执行顺序需要关注一下。load方法不同于其他覆盖方法在分类中的体现,如果类B本身中的其他方法在分类C中被重写,则会优先执行分类C中的。但是load不同,都会被执行到,因为这是类加载设置的方法。
3、出问题难排查
文本长按-编辑-复制或剪切的点击事件,需要过滤
摘自链接:https://www.jianshu.com/p/5499c7a4cba3