iOS中Method Swizzling-坑点总结

什么是Method Swizzling

实际上是方法交换。OC是个运行时语言,允许我们运行时修改方法,可以进行方法替换、交换等操作。方法交换实际就是互相替换方法的实现。以下是方法Method的底层结构:

struct method_t {
    SEL name;
    IMP imp;
    const char *types;
}

方法交换实际就是底层imp互相替换。关于方法替换可以参考官方文档。接下来要讨论的是方法交换在使用过程中应该注意的一些问题。

子类方法和父类方法替换导致父类调用异常

首先创建一个demo。自定义一个类Animal和继承自Animal的子类Dog。父类有个实例方法parentInstanceMethod,子类有个方法childInstanceMethod,对这两个方法进行替换,从中总结出一些问题。先来看一下demo。
interface部分:

@interface Animal : NSObject
- (void)parentInstanceMethod;
@end

@interface Dog : Animal
@end

@interface Dog(Addition)
- (void)childInstanceMethod;
@end

implementation部分:


@implementation Animal

- (void)parentInstanceMethod
{
    NSLog(@"%s", __func__);
}

@end

@implementation Dog
+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [Util my_instanceMethodSwizzlingWithClass:[self class] oriSel:@selector(parentInstanceMethod) swizzSel:@selector(childInstanceMethod)];
    });
}
@end

@implementation Dog(Addition)
- (void)childInstanceMethod
{
    [self childInstanceMethod];
    NSLog(@"%s", __func__);
}
@end

方法交换(Method Swizzling)代码逻辑:

+ (void)my_instanceMethodSwizzlingWithClass:(Class)cls oriSel:(SEL)oriSelecter swizzSel:(SEL)swizzSlecter
{
    if (!cls) NSLog(@"传入的交换类不能为空");
    Method oriMethod = class_getInstanceMethod(cls, oriSelecter);
    Method swizzMethod = class_getInstanceMethod(cls, swizzSlecter);
    method_exchangeImplementations(oriMethod, swizzMethod);
}

方法交换之后调用parentInstanceMethod方法:

    Animal *animal = [[Animal alloc] init];
    [animal parentInstanceMethod];
    
    Dog *dog = [[Dog alloc] init];
    [dog parentInstanceMethod];

但是问题来了,当运行的时候子类Dog调用parentInstanceMethod没有崩溃,父类Animal调用parentInstanceMethod崩溃了,为什么?

代码解析:首先因为Dog继承了Animal,所以相当于说Dog两个方法childInstanceMethod和parentInstanceMethod都有,但是Animal没有方法childInstanceMethod,所以在方法替换的时候,子类方法指向了父类方法parentInstanceMethod的实现,父类方法parentInstanceMethod指向了子类方法childInstanceMethod的实现,因此父类在调用parentInstanceMethod方法时,实际调用的是子类方法childInstanceMethod的实现,而此时子类中通过childInstanceMethod调用原先的父类方法,根据消息发送流程,实际上是向父类发送childInstanceMethod消息,但是父类方法列表中并没有childInstanceMethod方法,而在消息发送流程中,方法寻找过程是由子类向父类移动的,而方法childInstanceMethod存在于子类,所以就出现崩溃。那这个方法怎么解决呢?

方法交换前先尝试为当前类添加要被替换的方法

针对上面的问题,对原先的方法交换代码逻辑进行一下优化:

+ (void)my_instanceMethodSwizzlingWithClass:(Class)cls oriSel:(SEL)oriSelecter swizzSel:(SEL)swizzSlecter
{
    if (!cls) NSLog(@"传入的交换类不能为空");
    Method oriMethod = class_getInstanceMethod(cls, oriSelecter);
    Method swizzMethod = class_getInstanceMethod(cls, swizzSlecter);

    BOOL isSucceed = class_addMethod(cls, oriSelecter, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
    if (isSucceed) {//
        class_replaceMethod(cls, swizzSlecter, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
        method_exchangeImplementations(oriMethod, swizzMethod);
    }
}

在方法交换前,我们先尝试添加一个新的方法,如果添加成功,则直接替换,如果添加不成功则交换。
代码解析:这里oriMethod是parentInstanceMethod,swizzMethod是childInstanceMethod。首先会为clss添加一个新的方法,新的方法名与oriMethod相同,但是方法实现是swizzMethod的,如果添加成功,表示当前cls没有这个方法,然后只需要做个替换就可以了,如果当前类已经有这个方法,则进行交换。这样的做的结果就是不会修改父类的parentInstanceMethod实现。因为addMethod中判断cls是否有这个方法时只判断本类的而不会判断父类的。

但是这样就完美了吗?接下来还有一个问题。如果父类和子类都没没有实现parentInstanceMethod方法会有什么问题呢?

要替换的两个方法都判空,没有的话就先添加一个

针对上面的问题进一步优化,如下:

+ (void)my_instanceMethodSwizzlingWithClass:(Class)cls oriSel:(SEL)oriSelecter swizzSel:(SEL)swizzSlecter
{
    if (!cls) {
        NSLog(@"传入的交换类不能为空");
        return;
    };
    if(!oriSelecter && !swizzSlecter) {
        NSLog(@"传入两个空Slecter");
        return;
    };
    Method oriMethod = class_getInstanceMethod(cls, oriSelecter);
    Method swizzMethod = class_getInstanceMethod(cls, swizzSlecter);
    if(!oriMethod && !swizzMethod) {
        NSLog(@"两个方法不存在");
        return;
    };
    if (!oriMethod) {
        class_addMethod(cls, oriSelecter, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
//        method_setImplementation(swizzMethod, &parentMetodIMP);
        method_setImplementation(swizzMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
            NSLog(@"object:%@, _cmd:%p", self, _cmd);
        }));
    }else if (!swizzMethod) {
        class_addMethod(cls, swizzSlecter, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
//        method_setImplementation(oriMethod, &childMetodIMP);
        method_setImplementation(oriMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
            NSLog(@"object:%@, _cmd:%p", self, _cmd);
        }));
    }

    BOOL isSucceed = class_addMethod(cls, oriSelecter, method_getImplementation(swizzMethod), method_getTypeEncoding(swizzMethod));
    if (isSucceed) {//
        class_replaceMethod(cls, swizzSlecter, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
        method_exchangeImplementations(oriMethod, swizzMethod);
    }
}

这里面会先判断两个method是否为空,如果method为空则先添加一个,然后在进行交换。因不管是在添加或者交换的时候,如果方法为空是操作无效的。

总结

方法交换涉及到Method底层结构(SEL和IMP的关系),已经方法查找流程点击了解方查找流程,类的加载等内容。除了上面提到的要点,Method Swizzling使用过程中呢还有一些地方要注意,那就是如果是在+load方法实现,那意味着这个类要变成非懒加载类(详情参考类的加载流程),使用过多会影响启动速度(点击了解app启动优化),现在一般推荐在initialize方法里面实现,但是在使用initialize方法时得注意,如果类和分类同时实现的话只会执行分类的initialize。不管是写在哪里,首先都要保证在使用前进行交换,而且要保证只交换一次,以免错乱

你可能感兴趣的:(iOS中Method Swizzling-坑点总结)