UIButton的Category

  曾在网上看到过一个这样的文章iOS防止UIButton重复点击的三种实现方式,其中利用runtime来解决问题的思路很高端,但是使用过程中会碰到unrecognized selector sent to instance的问题。下面来解释产生这个问题的原因。

  源代码:

UIButton+TdxNoRepeatButton.h

#import <UIKit/UIKit.h>

@interface UIButton (TdxNoRepeatButton)

@property (nonatomic, assign) NSTimeInterval tdx_acceptEventInterval;       // 重复点击的间隔

@end

UIButton+TdxNoRepeatButton.m

#import "UIButton+TdxNoRepeatButton.h"
#import <objc/runtime.h>


// 因category不能添加属性,只能通过关联对象的方式。
static const char *UIControl_acceptEventInterval = "UIControl_acceptEventInterval";
static const char *UIControl_acceptEventTime = "UIControl_acceptEventTime";

@interface UIButton (TdxNoRepeatButton)

@property (nonatomic, assign,) NSTimeInterval tdx_acceptEventTime;           //当前点击时间

@end

@implementation UIButton (TdxNoRepeatButton)

- (NSTimeInterval)tdx_acceptEventInterval {
    return  [objc_getAssociatedObject(self, UIControl_acceptEventInterval) doubleValue];
}

- (void)setTdx_acceptEventInterval:(NSTimeInterval)tdx_acceptEventInterval {
    objc_setAssociatedObject(self, UIControl_acceptEventInterval, @(tdx_acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}


- (NSTimeInterval)tdx_acceptEventTime {
    return  [objc_getAssociatedObject(self, UIControl_acceptEventTime) doubleValue];
}

- (void)setTdx_acceptEventTime:(NSTimeInterval)tdx_acceptEventTime {
    objc_setAssociatedObject(self, UIControl_acceptEventTime, @(tdx_acceptEventTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}


// 在load时执行hook
+ (void)load {
    Method before   = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
    Method after    = class_getInstanceMethod(self, @selector(tdx_sendAction:to:forEvent:));
    method_exchangeImplementations(before, after);
}

- (void)tdx_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    if ([NSDate date].timeIntervalSince1970 - self.tdx_acceptEventTime < self.tdx_acceptEventInterval) {
        return;
    }
    
    if (self.tdx_acceptEventInterval > 0) {
        self.tdx_acceptEventTime = [NSDate date].timeIntervalSince1970;
    }
    
    [self tdx_sendAction:action to:target forEvent:event];
}


@end

unrecognized selector产生的原因

  我们在利用这个UIButton的Category时,如若遇到UITabBarButton、UISegmentedControl继承自UIControl的类就会产生崩溃,因为这些类并没有实现tdx_sendAction:to:forEvent函数。为什么会出现这种情况呢,我们明明是对UIButton进行Category扩展。首先我们来看看class_getInstanceMethod函数说明:

/** 
 * Returns a specified instance method for a given class.
 * 
 * @param cls The class you want to inspect.
 * @param name The selector of the method you want to retrieve.
 * 
 * @return The method that corresponds to the implementation of the selector specified by 
 *  \e name for the class specified by \e cls, or \c NULL if the specified class or its 
 *  superclasses do not contain an instance method with the specified selector.
 *
 * @note This function searches superclasses for implementations, whereas \c class_copyMethodList does not.
 */
OBJC_EXPORT Method class_getInstanceMethod(Class cls, SEL name)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0);

  注意note里面的话,这个函数会搜索父类的实现,而我们需要hook的函数正好是在UIControl中实现的,而不是UIButton,所以此处会hook所有的UIControl的sendAction:to:forEvent:函数。继而UITabBarButton、UISegmentedControl继承自UIControl的类都会被hook到,因此才会产生unrecognized selector错误。

解决方案

  既然知道产生问题的原因,那么下面我们来解决这个问题,有两种解决方案。

1、从UIControl入手。我们要hook的函数来自UIControl,那么我们可以为UIControl添加Category,而不是UIButton,这样所有继承自UIControl的类都会添加这个Category。

2、修改hook实现。修改load函数后的实现如下:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(sendAction:to:forEvent:);
        SEL swizzledSelector = @selector(tdx_sendAction:to:forEvent:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (success) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

  简单说下上述实现的思路。首先取到class、originalSelector、swizzledSelector、originalMethod、swizzledMethod,然后利用class_addMethod这个函数,此函数文档如下:

/** 
 * Adds a new method to a class with a given name and implementation.
 * 
 * @param cls The class to which to add a method.
 * @param name A selector that specifies the name of the method being added.
 * @param imp A function which is the implementation of the new method. The function must take at least two arguments—self and _cmd.
 * @param types An array of characters that describe the types of the arguments to the method. 
 * 
 * @return YES if the method was added successfully, otherwise NO 
 *  (for example, the class already contains a method implementation with that name).
 *
 * @note class_addMethod will add an override of a superclass's implementation, 
 *  but will not replace an existing implementation in this class. 
 *  To change an existing implementation, use method_setImplementation.
 */
OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp, 
                                 const char *types) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

  这个函数会在当前类上添加一个实例方法,并覆盖父类的方法返回YES。如果当前类有这个方法则无法覆盖返回NO。而我们hook的实现返回NO才会使用method_exchangeImplementations交换两者实现,这样有效的避免了替换掉当前类并未实现而在父类实现的方法,而所引发的unrecognized selector问题。

最近的文章

越狱环境下查找App沙盒目录

  在iOS开发中,能够随意更改app在真机上的文件可方便我们调试app,但自从iOS9以后app目录也越来越不好获取了。当然首先你得需要一个越狱手机,关于手机如何越狱,以下列出最近几个版本的越狱方法:iOS9.0-iOS9.1盘古官网iOS9.2-iOS9.3.3盘古官网会跳转到pp助手,使用win版pp助手点击一键越狱即可iOS10.0.0-iOS10.2此为越狱源码,开发者可自行下载编译,安装到手机上,然后根据提示来进行越狱  app沙盒目录结构图  下面来展示几种获取app沙盒目录...…

继续阅读
更早的文章

iOS静态库中的Category

  一个项目中使用了一个包含 category 的静态库,但是此项目在运行过程中,该静态库调用category增加的方法处,却报selector not recognized异常。产生的原因  苹果官方文档中的这个 Q&A QA1490:Building Objective-C static libraries with categories 已经说明了这个问题产生的原因:  这个异常是因为标准 UNIX 静态库、linker 以及 Objective-C 的动态性三者之间的实现导...…

继续阅读