美文网首页
Method swizzling的正确姿势

Method swizzling的正确姿势

作者: 初心丶可曾记 | 来源:发表于2017-10-25 17:33 被阅读382次

    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虽然能给我们带来很多便捷,但是调式困难,以及使用不当带来的麻烦也是难以排查,所以还是谨慎使用。

    相关文章

      网友评论

          本文标题:Method swizzling的正确姿势

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