Method swizzling的正确姿势

Method swizzling是通过runtime实现的方法交换,能够帮助在原有的方法里增加功能。以UIViewController的viewWillAppear:举例,一般我们交换方法新建一个UIViewController的category,然后在+load方法里面进行方法交换,一般有下面两种写法:

写法一:

#import <objc/runtime.h>

@implementation UIViewController (Swizzling)

+ (void)load {

    Method original = class_getInstanceMethod(self, @selector(viewWillAppear:));
    Method swizzling = class_getInstanceMethod(self, @selector(xxx_viewWillAppear:));

    method_exchangeImplementations(original, swizzling);
}

- (void)xxx_viewWillAppear:(BOOL)animated {
    
    [self xxx_viewWillAppear:animated];
}

@end

写法二:

#import <objc/runtime.h>

@implementation UIViewController (Swizzling)

+ (void)load {

    Method original = class_getInstanceMethod(self, @selector(viewWillAppear:));
    Method swizzling = class_getInstanceMethod(self, @selector(xxx_viewWillAppear:));

    BOOL successAdded = class_addMethod(self, @selector(viewWillAppear:), method_getImplementation(swizzling), method_getTypeEncoding(swizzling));

    if (successAdded) {
        class_replaceMethod(self, @selector(xxx_viewWillAppear:), method_getImplementation(original), method_getTypeEncoding(original));
    } else {
        method_exchangeImplementations(original, swizzling);
    }


}
- (void)xxx_viewWillAppear:(BOOL)animated {
    
    [self xxx_viewWillAppear:animated];
    
}

@end

两种方法代码运行之后均没有问题,我们暂且先来看另一种情况,继承UIViewController创建一个子类CustomViewController,然后添加个一个CustomViewController的分类注意将之前的UIViewController(Swizzling)文件删除,此时项目中的文件结构如下

image

同样的按之前的写法一进行方法交换

image

运行之后出现崩溃,错误日志如下
image

接来下我们换成写法二的方式再试一次

image

运行之后一切正常,我们分析一下写法一写法二的差别就在写法二先给类添加方法,添加成功就替换它的实现,添加失败则进行方法交换,在CustomViewController中,我并没有重写viewWillAppear:方法,所以如果此时直接将两个方法进行交换,实际上是将它的父类UIViewController的viewWillAppear和xxx_viewWillAppear方法进行交换。

而UIViewController是所有VC的父类,当ViewController即将出现的时候,viewWillAppear会调用(注意ViewController没有重写viewWillAppear,故此时是调用父类UIViewController的方法),因为交换了方法实现,此时就是调用到CustomViewController(Swizzling)分类中的xxx_viewWillAppear方法,而xxx_viewWillAppear是属于CustomViewController的方法,所以会出现之前的

[ViewController xxx_viewWillAppear:]: unrecognized selector sent to instance 0x7f8f53406140

改用写法二之后,首先先向CustomViewController添加viewWillAppear方法,添加成功后直接替换它的方法实现,如果添加失败说明这个类本身就有该方法,则直接进行方法交换。用这种方式无论类中是否有这个方法,都能保证交换的方法是在本类中的方法,而不会影响到父类方法的交换,因此就保证了安全。

如果你以为上面的写法就没有任何问题,我只能说你图样图森破。我们来看另一种场景,UIViewController+Swizzling中交换viewWillAppear, CustomViewController+ Swizzling(CustomViewController中没实现viewWillAppear方法)中交换viewWillAppear,并且都打印NSLog(@"%@",[self Class]);我们期望的打印顺序是:

UIViewController
CustomViewController

然而实际的打印可能是:

CustomViewController

注意我这里用的是可能,我们是在+ (void)load方法进行方法交换,load方法的加载能保证父类优先,然后再是子类,但是分类的load加载时机没有固定顺序,这个和target->build phases->complie Sources的文件顺序有关,按照从上到下的顺序加载,所以如果CustomViewController+Swizzling在UIViewController+Swizzling上面,即先加载的话,则CustomViewController会先addMethod,此时是copy了父类的方法添加进来然后交换,交换的是原始父类的实现。然后UIViewController+Swizzling加载,交换的方法不会影响CustomViewController+Swizzling,所以此时的打印就是只有

CustomViewController

所以要想达到预期的效果,一个是调整target->build phases->complie Sources的文件顺序,另一个就是当子类没有重写父类方法时,我们在交换的方法中去动态查找父类的实现,下面看下代码如何实现。

首先在CustomViewController+Swizzling定义了一个全局block

#import "CustomViewController+Swizzling.h"
#import <objc/runtime.h>

static void (^callOriginalViewWillAppearBlock)(id,SEL,BOOL);

@implementation CustomViewController (Swizzling)

然后在class_addMethod成功后设置这个全局block

+ (void)load {
    Method original = class_getInstanceMethod(self, @selector(viewWillAppear:));
    Method swizzling = class_getInstanceMethod(self, @selector(custom_viewWillAppear:));
    BOOL successAdded = class_addMethod(self, @selector(viewWillAppear:), method_getImplementation(swizzling), method_getTypeEncoding(swizzling));

    if (successAdded) {
        class_replaceMethod(self, @selector(custom_viewWillAppear:), method_getImplementation(original), method_getTypeEncoding(original));
        //重点在这里,重点在这里,重点在这里
        void(^callOriginalBlcok)(id,SEL,BOOL) = ^(id self, SEL cmd,BOOL animated) {
            //拿到父类
            Class superClass = class_getSuperclass([self class]);
            Method original = class_getInstanceMethod(superClass, cmd);
            //获取父类的函数指针
            IMP originalIMP_ = method_getImplementation(original);
            //强转函数指针的类型
            void(*originalIMP)(id,SEL,BOOL);
            originalIMP = (__typeof(originalIMP))originalIMP_;
            //函数调用
            originalIMP(self,cmd,animated);
        
    };
    callOriginalViewWillAppearBlock = callOriginalBlcok;
    } else {
        method_exchangeImplementations(original, swizzling);
    }
}

首先是拿到它的父类,获取父类的Method以及函数指针IMP,进入xcode看下IMP的结构


IMP@2x.png

因为xcode做了一些限制,我们使用IMP时的类型是上图中标签1处的类型,所以我们这里强转一下类型,然后拿到函数指针之后调用。
最后是交换的@selector方法

- (void)xxx_viewWillAppear:(BOOL)animated {
    if (callOriginalViewWillAppearBlock) {
        callOriginalViewWillAppearBlock(self,@selector(viewWillAppear:),animated);
    } else {
        [self xxx_viewWillAppear:animated];
    }
    NSLog(@"CustomViewController category");
}

判断全局block是否为nil,不为nil,则表示动态查找父类的实现;为nil,则直接调用。

这里只是简单介绍了一下正确swizzle的思路,还有很多不完善的地方,推荐大家去看RSSwizzle,swizzle思路也是动态查找父类的实现,但它的实现方式十分优雅,非常值得一看。最后,Method swizzling虽然能给我们带来很多便捷,但是调式困难,以及使用不当带来的麻烦也是难以排查,所以还是谨慎使用。