美文网首页
自己研究的runtime 粗劣版。自己学习过程

自己研究的runtime 粗劣版。自己学习过程

作者: NJKNJK | 来源:发表于2021-05-14 08:58 被阅读0次

    class_getInstanceMethod 得到类的实例方法
    class_getClassMethod 得到类的类方法

    Method originalMethod = class_getInstanceMethod(cls, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector);
    

    无论是自己的扩展方法,还是原类的方法。都可以获得。(目前是使用过实现的方法)

    思路:利用runtime实现方法交换(method_exchangeImplementations)和利用runtime 给分类动态绑定属性timeInterval和isIgnoreEvent。timeInterval供外界访问设置点击的间隔时间,isIgnoreEvent为私有的属性,用来判断是否点击过,点击过就返回,没点击过就执行点击事件。

    效果:按钮被点击之后,在设定时间内不可以再次响应点击事件。可有效防止暴力点击。

    具体实现如下(代码中有必要的注释):

    UIButton+CLTouch.h文件代码

    @interface UIButton (CLTouch)
    
    /**设置点击时间间隔*/
    @property (nonatomic, assign) NSTimeInterval timeInterval;
    
    @end
    
    

    UIButton+CLTouch.m文件代码

     #import "UIButton+CLTouch.h"
    
    #define defaultInterval 2.0 //默认时间间隔
    
    @interface UIButton()
    /**bool 类型 YES 不允许点击   NO 允许点击   设置是否执行点UI方法*/
    @property (nonatomic, assign) BOOL isIgnoreEvent;
    
    @end
    
    @implementation UIButton (CLTouch)
    + (void)load{
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            SEL selA = @selector(sendAction:to:forEvent:);
            SEL selB = @selector(mySendAction:to:forEvent:);
            Method methodA =   class_getInstanceMethod(self,selA);
            Method methodB = class_getInstanceMethod(self, selB);
            //将 methodB的实现 添加到系统方法中 也就是说 将 methodA方法指针添加成 方法methodB的  返回值表示是否添加成功
            BOOL isAdd = class_addMethod(self, selA, method_getImplementation(methodB), method_getTypeEncoding(methodB));
            //添加成功了 说明 本类中不存在methodB 所以此时必须将方法b的实现指针换成方法A的,否则 b方法将没有实现。
            if (isAdd) {
                class_replaceMethod(self, selB, method_getImplementation(methodA), method_getTypeEncoding(methodA));
            }else{
                //添加失败了 说明本类中 有methodB的实现,此时只需要将 methodA和methodB的IMP互换一下即可。
                method_exchangeImplementations(methodA, methodB);
            }
        });
    }
    
    //当我们按钮点击事件 sendAction 时  将会执行  mySendAction
    - (void)mySendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
    {
        if ([NSStringFromClass(self.class) isEqualToString:@"UIButton"]) {
    
            self.timeInterval = self.timeInterval == 0 ?defaultInterval:self.timeInterval;
            if (self.isIgnoreEvent){
                return;
            }else if (self.timeInterval > 0){
                [self performSelector:@selector(resetState) withObject:nil afterDelay:self.timeInterval];
            }
        }
        //此处 methodA和methodB方法IMP互换了,实际上执行 sendAction;所以不会死循环
        self.isIgnoreEvent = YES;
        [self mySendAction:action to:target forEvent:event];
    }
    
    //runtime 动态绑定 属性
    - (void)setIsIgnoreEvent:(BOOL)isIgnoreEvent{
        // 注意BOOL类型 需要用OBJC_ASSOCIATION_RETAIN_NONATOMIC 不要用错,否则set方法会赋值出错
        objc_setAssociatedObject(self, @selector(isIgnoreEvent), @(isIgnoreEvent), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    - (BOOL)isIgnoreEvent{
        //_cmd == @select(isIgnore); 和set方法里一致
        return [objc_getAssociatedObject(self, _cmd) boolValue];
    }
    - (void)resetState{
        [self setIsIgnoreEvent:NO];
    }
    
    //给button添加timeInterval属性实现其get和set方法
    - (NSTimeInterval)timeInterval
    {
        return [objc_getAssociatedObject(self, _cmd) doubleValue];
    }
    
    - (void)setTimeInterval:(NSTimeInterval)timeInterval
    {
        objc_setAssociatedObject(self, @selector(timeInterval), @(timeInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    

    但是实际操作的时候,恶意的在短时间内多次点击按钮,按钮的点击事件不是一次,偶尔会出现两次。这是因为点击之后需要有个反应时间。如果是要求不严格这机会可以使用了。但有的时候严格要求:点击按钮只能响应一次的需求了,这就显得有点尴尬了。

    按钮点击触发的方法:在0.3秒时间间隔内多次点击只响应一次点击事件。之后配合上面的方法就可以保证只能响应一次的需求了。

    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(handleSendSmsResult:) object:button];
    [self performSelector:@selector(handleSendSmsResult:) withObject:button afterDelay:0.3f];
    

    前话

    这几天在系统的学习 runtime,在学习 runtime 的基础使用案例中,"方法替换"这种使用情况下,发现有两种写法. 其实也不是两种写法,准确的来说一种是比较严谨的,另一种则没有那么严谨.

    发现这两种写法的差异后,我主要集中在下列:

    • class_addMethod
    • class_replaceMethod
    • method_exchangeImplementations

    哪个方法的具体作用.

    下面,这篇文章就这两种写法和上述三种方法的区别.

    第一种写法

    《OC最实用的runtime总结,面试、工作你看我就足够了!》的时候,它里边的写法是简单的获取到被替换和替换方法的Method.然后直接使用method_exchangeImplementations进行方法的替换. 最开始使用的时候,因为测试范例比较简单,所以并没有发现这样写的弊端.但是确实能够实现方法替换的效果. 代码如下:

    +(void)load{
        //获取两个类的方法
        Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
        Method m2 = class_getClassMethod([UIImage class], @selector(ll_imageName:));
        //开始交换方法实现
        method_exchangeImplementations(m1, m2);
    }
    
    

    在后来看到《runtime详解》的时候,发现作者的写法并不是这样,虽然作者添加少量注释,但是愚钝的我还没有想清楚,这也是这篇文章的初衷,也是下一小结的由来.

    第二种写法

    上一节的这种情况虽然能够实现我们想要的效果.但是我们有没有想过这种情况:

    " 周全起见,有两种情况要考虑一下。第一种情况是要复写的方法(overridden)并没有在目标类中实现(notimplemented),而是在其父类中实现了。第二种情况是这个方法已经存在于目标类中(does existin the class itself)。这两种情况要区别对待。 (译注: 这个地方有点要明确一下,它的目的是为了使用一个重写的方法替换掉原来的方法。但重写的方法可能是在父类中重写的,也可能是在子类中重写的。) 对于第一种情况,应当先在目标类增加一个新的实现方法(override),然后将复写的方法替换为原先(的实现(original one)。 对于第二情况(在目标类重写的方法)。这时可以通过method_exchangeImplementations来完成交换."

    ---- 以上来自:《Objective-C的方法替换》

    +(void)load{
        NSString *className = NSStringFromClass(self.class);
        NSLog(@"classname %@", className);
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            //要特别注意你替换的方法到底是哪个性质的方法
            // When swizzling a Instance method, use the following:
            //        Class class = [self class];
    
            // When swizzling a class method, use the following:
            Class class = object_getClass((id)self);
    
            SEL originalSelector = @selector(systemMethod_PrintLog);
            SEL swizzledSelector = @selector(ll_imageName);
    
            Method originalMethod = class_getInstanceMethod(class, originalSelector);
            Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
            BOOL didAddMethod =
            class_addMethod(class,
                            originalSelector,
                            method_getImplementation(swizzledMethod),
                            method_getTypeEncoding(swizzledMethod));
    
            if (didAddMethod) {
                class_replaceMethod(class,
                                    swizzledSelector,
                                    method_getImplementation(originalMethod),
                                    method_getTypeEncoding(originalMethod));
            } else {
                method_exchangeImplementations(originalMethod, swizzledMethod);
            }
        });
    }
    
    

    解析:

    上面提到的:

    dispatch_once这里不是“单例”,是保证方法替换只执行一次.

    说明:

    systemMethod_PrintLog:被替换方法ll_imageName:替换方法

    class_addMethod:如果发现方法已经存在,会失败返回,也可以用来做检查用,我们这里是为了避免源方法没有实现的情况;如果方法没有存在,我们则先尝试添加被替换的方法的实现

    1.如果返回成功:则说明被替换方法没有存在.也就是被替换的方法没有被实现,我们需要先把这个方法实现,然后再执行我们想要的效果,用我们自定义的方法去替换被替换的方法. 这里使用到的是class_replaceMethod这个方法. class_replaceMethod本身会尝试调用class_addMethodmethod_setImplementation,所以直接调用class_replaceMethod就可以了)

    2.如果返回失败:则说明被替换方法已经存在.直接将两个方法的实现交换即

    另外:

    • 我们可以利用 method_exchangeImplementations 来交换2个方法中的IMP
    • 我们可以利用 class_replaceMethod 来修改类
    • 我们可以利用 method_setImplementation 来直接设置某个方法的IMP

    其实我们如果 研究过 AFN 代码的话,会发现, AFN 就是第二种写法.在AFURLSessionManager.m的第296行:

    static inline void af_swizzleSelector(Class class, SEL originalSelector, SEL swizzledSelector) {
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        if (class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    
    }
    
    

    详尽的代码请查看 Demo.

    下载地址

    具体的 Demo 代码可以在我的 GitHub 上找到 Demo地址

    其它

    关于 load 的调用次数问题,大家可以查看这两篇文章.+(void)load和+(void)initialize可当做普通类方法(Class Method)调用的.《NSObject的load和initialize方法!》《Objective C类方法load和initialize的区别》

    相关文章

      网友评论

          本文标题:自己研究的runtime 粗劣版。自己学习过程

          本文链接:https://www.haomeiwen.com/subject/ycpadltx.html