iOS runtime、runloop、多线程iOS 收藏夹架构

iOS 札记1:Method Swizzling小记

2017-09-13  本文已影响516人  南华coder

导语:Method Swizzling是Objective-C中运行时中讨论较多的内容,本文主要介绍使用Method Swizzling遇到的问题项目中使用的Swizzling方案

一、Method Swizzling简介

Method Swizzling的本质是在运行时交换方法实现(IMP),如hook系统方法,在原有的方法中,插入自己的业务需求。

1、Method Swizzling原理
MethodLists示意图.png hook后的MethodLists示意图.png
2、Method Swizzling使用

Method Swizzling的本质就是偷换selector的IMP,下面就Swizzle NSObject的description方法,简单举例:

#import "NSObject+Swizzle.h"
#import <objc/runtime.h>

@implementation NSObject (Swizzle)

+ (void)load{
   //调换IMP
    Method originalMethod = class_getInstanceMethod([NSObject class], @selector(description));
    Method myMethod = class_getInstanceMethod([NSObject class], @selector(qs_description));
    method_exchangeImplementations(originalMethod, myMethod);
}

- (void)qs_description{
    NSLog(@"description 被 Swizzle 了");
    return [self qs_description];    
}
@end

说明:调用被hook的description方法,获取内容前,会打印“description 被 Swizzle 了”这样的日志。

3、Method Swizzling存在的问题

二、RSSwizzle:Method Swizzling的优雅方案

RSSwizzle线程安全的Method Swizzling方案,能够帮我们解决Method Swizzling的使用问题。介绍如下:

1、不是线程安全的(Method swizzling is not atomic)
2、 改变了代码本来的行为(Changes behavior of un-owned code)
3、潜在的命名冲突(Possible naming conflicts)#####
4、改变方法的参数(Swizzling changes the method's arguments)
5、继承问题(The order of swizzles matters)
6、难以理解 (Difficult to understand)
7、难以调试(Difficult to debug)

三、RSSwizzle的基础使用

RSSwizzle中提供了两种使用方式,一种是通过调用类方法来实现函数的替换,另一种是使用RSSwizzle定义的来进行函数的替换。

1、 使用类方法替换实例方法实现
/**
 参数1:要被替换的函数选择器
 参数2:要被替换的函数所在的类
 参数3: block中返回替换后的方法,block参数中需要返回一个方法函数,这个函数为要替换成的函数,要和原函数类型相同。在类中的函数默认都会有一个名为self的id参数
 参数4:此次替换用到的key
 */
[RSSwizzle swizzleInstanceMethod:@selector(touchesBegan:withEvent:) inClass:[ViewController class] newImpFactory:^id(RSSwizzleInfo *swizzleInfo) {
    return ^(__unsafe_unretained id self,NSSet* touches,UIEvent* event){
        NSLog(@"touchesBegan:withEvent:被Swizzle了");
    };
} mode:RSSwizzleModeAlways key:NULL];
2、 使用宏替换实例方法实现
 /*
 参数1:要被替换的函数所在的类
 参数2: 要被替换的函数选择器
 参数3:返回值类型,
 参数4:参数列表
 参数5:要替换的代码块,
 参数6:执行模式,
 参数7:key值标识,RSSwizzleModeOncePerClass模式下使用,其他情况置为NULL
 */
RSSwizzleInstanceMethod([ViewController class], @selector(touchesEnded:withEvent:), RSSWReturnType(void), RSSWArguments(NSSet<UITouch *> *touches,UIEvent *event),RSSWReplacement({
    
    NSLog(@"touchesEnded:withEvent被Swizzle了");
    RSSWCallOriginal(touches,event);
}), RSSwizzleModeAlways, NULL);    
3、 使用类方法替换类方法实现
/*
 参数1:要替换的函数选择器
 参数2:要替换此函数的类
 参数3:block中返回替换后的方法,block参数中需要返回一个方法函数,这个函数为要替换成的函数,要和原函数类型相同。在类中的函数默认都会有一个名为self的id参数
 */
[RSSwizzle swizzleClassMethod:@selector(testClassMethod1) inClass:[ViewController class] newImpFactory:^id(RSSwizzleInfo *swizzleInfo) {
    
    return ^(__unsafe_unretained id self){
        NSLog(@"Class testClassMethod1 Swizzle");
    };
}];
4、使用宏替换类方法实现
/*
 参数1:要替换方法的类
 参数2:要替换的方法选择器
 参数3:方法的返回值类型
 参数4:方法的参数列表
 参数5:要替换的方法代码块
 */
RSSwizzleClassMethod(NSClassFromString(@"ViewController"), NSSelectorFromString(@"testClassMethod2"), RSSWReturnType(void), RSSWArguments(), RSSWReplacement({
    //先执行原始方法
    RSSWCallOriginal();
    NSLog(@"Class testClassMethod2 Swizzle");
}));

说明:RSSwizzle还提供了Swizzle模式,使用Swizzle实例方法时候需要用到。Swizzle类方法,默认RSSwizzleModeAlways,定义如下:

typedef NS_ENUM(NSUInteger, RSSwizzleMode) {
    //任何情况下 始终执行替换操作
    RSSwizzleModeAlways = 0,
    //相同key标识的替换操作只会被执行一次
    RSSwizzleModeOncePerClass = 1,
    //相同key标识的替换操作在子类父类中只会被执行一次
    RSSwizzleModeOncePerClassAndSuperclasses = 2
};

四、一个使用Swizzling典型的错误案例

网络上很多博客介绍了使用Swizzling来防止重复点击UIButton,但是大部分都会有问题。

1、错误代码

一般在load中替换sendAction:to:forEvent:方法,主要代码如下:

+ (void)load {
    Method before   = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
    Method after    = class_getInstanceMethod(self, @selector(qs_sendAction:to:forEvent:));
    method_exchangeImplementations(before, after);
}

- (void)qs_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    if ([NSDate date].timeIntervalSince1970 - self.qs_acceptEventTime < self.qs_acceptEventInterval) {
        return;
    }

    if (self.qs_acceptEventInterval > 0) {
        self.qs_acceptEventTime = [NSDate date].timeIntervalSince1970;
    } 
    [self qs_sendAction:action to:target forEvent:event];
 }

错误现象

点击UITabBar上按钮会crash, 提示类似于:[UITabBarButton qs_acceptEventTime]: unrecognized selector sent to instance ...。

错误原因

1)UITabBarButton是UITabBarController中各个子控制器在工具条中对应的按钮,是UITabBar的私有属性,UITabBarButton的父类是UIControl,而UIButton的父类也是UIControl,sendAction:to:forEvent:是UIControl的实例方法;

2) 在UIButton类中没有sendAction:to:forEvent:这个方法实现,通过class_getInstanceMethod() 获取的是父类的 Method 对象,使用 method_exchangeImplementations() 就把父类的原始实现(IMP)跟自己的 Swizzle 实现交换了。这就导致UIControl的其他子类,如UITabBarButton在被点击后,都调用了UIButton的Swizzle 实现,发生了严重的Crash问题。

说明:虽然在UIControl的分类的load方法交换方法实现,能解决问题,我们将Swizzling的影响扩大很多倍,不是理想的做法。下面介绍解决办法。

2、解决办法

在项目直接使用method_exchangeImplementations很危险,甚至导致Crash,在项目中不建议这么做。可采用的解决办法有两种:

方法A

原理:如果类中没有实现 Original Selector 对应的方法,那就通过class_addMethod方法为Original Selector增加Swizzle 的实现,通过class_replaceMethod修改Swizzle Selector 的 实现 为 Original 的实现;如果已经有Original Selector 对应的方法(通过class_addMethod方法添加是失败的), 这时才使用method_exchangeImplementations来直接交换。

代码如下

+ (void)load {

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        SEL originalSelector = @selector(sendAction:to:forEvent:);
        SEL swizzledSelector = @selector(qs_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);
        }
    });
}

说明1:class_addMethod方法可以为类添加新的方法实现(IMP),添加成功返回YES.。否则返回NO。如果选择器(select)已经有对应的方法实现(IMP), 添加也是失败的,利用这点可以检查是否有源方法实现,如果没有利用class_replaceMethod来将swizzledSelector和originalMethod对应设置好。

说明2:.class_replaceMethod用来替换类中的方法实现,会调用class_addMethod和method_setImplementation方法(直接设置某个方法的IMP)

方法B

原理:RSSwizzle完美避开了在load中使用method_exchangeImplementations交换方法的尴尬,基于Swizzle模式和class_replaceMethod完美控制了替换方法实现。

代码如下

+ (void)load{
    RSSwizzleInstanceMethod([UIButton class], @selector(sendAction:to:forEvent:), RSSWReturnType(void), RSSWArguments(SEL action,id target,UIEvent *event), RSSWReplacement({
           UIButton *btn = self;
            if ([NSDate date].timeIntervalSince1970 - btn.qs_acceptEventTime < btn.qs_acceptEventInterval) {
                return;
            }      
            if (btn.qs_acceptEventInterval > 0) {
                btn.qs_acceptEventTime = [NSDate date].timeIntervalSince1970;
            }        
            RSSWCallOriginal(action,target,event);    
    }), RSSwizzleModeAlways, NULL);
}

说明:RSSwizzleInstanceMethod宏实现方法实现的替换,代码更易阅读。

End

上一篇下一篇

猜你喜欢

热点阅读