Method-swizzling 方法交换

有时候因为各种需求,我们可能需要给某些系统类或者SDK的类的方法中,添加某些自己的代码。这时候我们就需要用到 Method-swizzling

方法交换

简单来说就是通过runtime中的method_exchangeImplementations 方法,交换2个方法的sel指向,让他们指向另一个imp。

例如:有一个BClass类,其中有func1方法

@implementation BClass
- (void)func1 {
    NSLog(@"BClass: func1");
}

@end
复制代码

现在我们给他添加一个分类,在分类中用func2替换func1的实现

@implementation BClass (MethodSwizzling)
+ (void)load {
    Method original = class_getInstanceMethod(self, @selector(func1));
    Method replace = class_getInstanceMethod(self, @selector(func2));
    method_exchangeImplementations(original, replace);
}
- (void)func2 {
    NSLog(@"BClass: func2");
}
@end
复制代码

现在当我们使用func1

BClass *t = [BClass new];
[t func1]; // 输出 BClass: func2
[t func2]; // 输出 BClass: func1
复制代码

可见现在的sel与imp的关系是:

image.png

而原始关系是:

image.png

给方法添加内容

上面的方法在调用func1是,直接执行了func2的实现,但一般我们是想在func1方法执行之前或之后,加一些自己的操作,还是需要执行func1的实现的。 这个其实也很好实现,我们只需要在func2中调用一下func1就好了。

- (void)func2 {
    NSLog(@"func1 之前");
    [self func2];
    NSLog(@"func1 之后");
}
复制代码

现在当我们使用func1

BClass *t = [BClass new];
[t func1]; 
打印: 
func1 之前
BClass: func1
func1 之后
复制代码

这里需要注意的是,func2中调用func1时写的是[self func2];,这是因为上面进行了方法交换,现在通过func2的sel才能找到func1的imp。

优化方法交换

上面虽然实现了我们的需求,但是不够健壮,有些时候可能会出现bug。 我们先修改一下func2,方便观察打印

- (void)func2 {
    NSLog(@"BClass: func2");
    [self func2];
}
复制代码

防止多次调用load,导致方法交换还原

load方法是可以代码调用的,万一有人代码调用了一下,那我们的交换就会再次交换,也就还原了。虽然这个可能性非常小,但也有可能的。

BClass *t = [BClass new];
[t func1];
/*
打印
BClass: func2
BClass: func1
*/
[BClass load];
[t func1];
/*
打印
BClass: func1
*/
复制代码

对此,我们可以给代码加上仅能执行一次的防御,这样就不会出现交换回去的情况了。

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method original = class_getInstanceMethod(self, @selector(func1));
        Method replace = class_getInstanceMethod(self, @selector(func2));
        method_exchangeImplementations(original, replace);
    });
}
复制代码

存在继承关系的类的方法

现在让BClass继承自AClass,然后func1AClass的方法,BClass中没有实现。

@implementation AClass
- (void)func1 {
    NSLog(@"AClass: func1");
}
@end

@interface BClass : AClass
@end
复制代码

我们使用一下

BClass *t = [BClass new];
[t func1];
/*
打印
BClass: func2
BClass: func1
*/
AClass *a = [AClass new];
[a func1];
/*
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason:'-[AClass func2]: unrecognized selector sent to instance 0x600003bb8520'
*/
复制代码

发现当我们使用AClass的实例,调用func1时会报错,找不到func2的实现。

其实原因也很简单,因为BClass为实现func1,所以获取Method的时候,拿到的其实是AClassfunc1,也就是说把AClassfunc1BClassfunc2进行了方法交换。所以BClass的实例使用正常,而AClass的实例使用就错误了。

显然,这种存在继承关系的类方法很常见的。所以我们进行方法交换的时候,不能简单的直接交换,要保证交换的原方法一定是当前类,而不是父类的。

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originalSEL = @selector(func1);
        SEL replaceSEL = @selector(func2);
        Method original = class_getInstanceMethod(self, originalSEL);
        Method replace = class_getInstanceMethod(self, replaceSEL);
        // 尝试向BClass中添加一个 func1 的 sel,指向 fun2 imp 的方法
        BOOL addMethod = class_addMethod(self, originalSEL, method_getImplementation(replace), method_getTypeEncoding(replace));
        // 如果添加成功,说明类本身没有实现 func1,添加失败,则表示类本身实现了 func1
        if (addMethod) {
            // 类原来未实现 func1,但是我们添加了一个 imp 是 func2 的 func1
            // 所以,我们只要再让 func2 的 sel 指向 func1 的 imp,其实就能完成方法交换了
            class_replaceMethod(self, replaceSEL, method_getImplementation(original), method_getTypeEncoding(original));
        }else{
            // 既然类实现了 func1 ,那么可以直接交换了
            method_exchangeImplementations(original, replace);
        }
    });
}
复制代码

image.png

如果原方法未实现,上面优化后的交换会导致死循环

如果AClassfunc1只是声明了,并未实现,BClass也未实现,那么

BClass *t = [BClass new];
[t func1];
/*
一直打印 BClass: func2
*/
复制代码

这其实是因为,func1imp,不存在,导致 class_replaceMethod方法更改 func2 指向失败,func2 依旧指向原来的 imp导致的

image.png

所以,我们可以给 func1 添加一个空的实现

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SEL originalSEL = @selector(func1);
        SEL replaceSEL = @selector(func2);
        Method original = class_getInstanceMethod(self, originalSEL);
        Method replace = class_getInstanceMethod(self, replaceSEL);
        // 如果原方法未实现,给添加一个空的实现
        if (!original) {
            // 添加方法,注意这里因为original,不存在,所以使用replace的信息添加的方法,这是的imp是指向 replace 的
            class_addMethod(self, originalSEL, method_getImplementation(replace), method_getTypeEncoding(replace));
            // 重新设置 imp,这个很关键,因为这里originalSEL才指向了新的imp,才能正常进行下面的方法交换
            method_setImplementation(replace, imp_implementationWithBlock(^(id self, SEL _cmd){}));
        }
        BOOL addMethod = class_addMethod(self, originalSEL, method_getImplementation(replace), method_getTypeEncoding(replace));
        if (addMethod) {
            class_replaceMethod(self, replaceSEL, method_getImplementation(original), method_getTypeEncoding(original));
        }else{
            method_exchangeImplementations(original, replace);
        }
    });
}
复制代码

image.png

猜你喜欢

转载自juejin.im/post/7106728154646970405