Runtime-防止重复点击-UIButton、UITableView

Gesture有系统处理单机双击,暂不去自定义时间间隔了。只处理UIButton、UITableView(UICollectionView)

1、思路:

UIButton hook sendAction
UITableView hook setDelegate(swift中没有此方法,改用OC)
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)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、出问题难排查
文本长按-编辑-复制或剪切的点击事件,需要过滤
右滑删除case:会快速调用两遍sendAction方法

iOS利用runtime,解决多次点击相同button,导致重复跳转的问题
IOS切面编程Hook
iOS无埋点数据统计实践
OC 对同一个方法进行多次交换
method swizzling你应该注意的点
Runtime使用场景-埋点、重复点击、数组越界崩溃

你可能感兴趣的:(Runtime-防止重复点击-UIButton、UITableView)