美文网首页iOS 实用iOS技术点iOS 开发每天分享优质文章
浅谈 Method Swizzling 中遇到的一些问题

浅谈 Method Swizzling 中遇到的一些问题

作者: ifelseboyxx | 来源:发表于2017-01-27 22:41 被阅读256次

    如果对 Runtime 有一定了解的话,一定听说过或者用过这个函数:

    OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) 
    

    它通常叫做 Method Swizzling,算是objc的 “黑魔法” 了,作用就是在程序运行期间动态的给两个方法互换实现。

    最近有用到这个,总结下遇到的一些问题:

    静态(类)方法和实例方法的交换实现方式一样吗?

    交换静态(类)方法的正确姿势:

    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            SEL s1 = @selector(go);
            SEL s2 = @selector(stop);
            Class class = object_getClass((id)self);
            Method m1 = class_getClassMethod(class, s1);
            Method m2 = class_getClassMethod(class, s2);
            BOOL success = class_addMethod(class, s1, method_getImplementation(m2), method_getTypeEncoding(m2));
            if (success){
                class_replaceMethod(class, s2, method_getImplementation(m1), method_getTypeEncoding(m1));
            }
            else{
                method_exchangeImplementations(m1, m2);
            }
        });
    }
    
    + (void)go {
        NSLog(@"Go!");
    }
    
    + (void)stop {
        NSLog(@"Stop!");
    }
    
    

    交换实例方法的正确姿势:

    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            SEL s1 = @selector(go);
            SEL s2 = @selector(stop);
            Class class = [self class];
            Method m1 = class_getInstanceMethod(class, s1);
            Method m2 = class_getInstanceMethod(class, s2);
            BOOL success = class_addMethod(class, s1, method_getImplementation(m2), method_getTypeEncoding(m2));
            if (success){
                class_replaceMethod(class, s2, method_getImplementation(m1), method_getTypeEncoding(m1));
            }
            else{
                method_exchangeImplementations(m1, m2);
            }
        });
    }
    
    - (void)go {
        NSLog(@"Go!");
    }
    
    - (void)stop {
        NSLog(@"Stop!");
    }
    
    

    我们可以发现上面两段方法的区别在于:

    静态(类)方法的ClassMethod是这样的:

    Class class = object_getClass((id)self);
    Method m1 = class_getClassMethod(class, s1);
    Method m2 = class_getClassMethod(class, s2);
    

    实例方法的ClassMethod是这样的:

    Class class = [self class];
    Method m1 = class_getInstanceMethod(class, s1);
    Method m2 = class_getInstanceMethod(class, s2);
    

    Runtime 中class_getClassMethod的实现:

    Method class_getClassMethod(Class cls, SEL sel)
    {
        if (!cls  ||  !sel) return nil;
    
        return class_getInstanceMethod(cls->getMeta(), sel);
    }
    
    

    大家可以下载 Runtime 源码 看看。

    其实class_getClassMethod内就是调了下class_getInstanceMethod,只是传的Class参数不一样:

    • class_getClassMethodClass参数传的是元类,也就是类对象的类。(关于元类大家可以看看这篇翻译的文章 Objective-C 中的元类(meta class)是什么?
    • class_getInstanceMethodClass 参数看名字就可以理解,既然是获得实例方法,自然传的就是实例对象的类。

    object_getClass(id obj) 又是什么呢?还是看源码:

    Class object_getClass(id obj)
    {
        if (obj) return obj->getIsa();
        else return Nil;
    }
    
    

    object_getClass() 就是顺着 isa 的指向链找到对应的类。

    有兴趣的可以看看这篇文章:为什么 object_getClass(obj) 与 [OBJ class] 返回的指针不同

    静态(类)方法和实例方法里面的 self 表示的含义一样吗?

    先说明下为什么我会突然有这个疑问:

    当初为了方便测试交换两个静态方法的实现,我直接撸了这一段代码:

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            SEL s1 = @selector(test1);
            SEL s2 = @selector(test2);
            Class class = object_getClass((id)self);
            Method m1 = class_getClassMethod(class, s1);
            Method m2 = class_getClassMethod(class, s2);
            BOOL success = class_addMethod(class, s1, method_getImplementation(m2), method_getTypeEncoding(m2));
            if (success){
                class_replaceMethod(class, s2, method_getImplementation(m1), method_getTypeEncoding(m1));
            }
            else{
                method_exchangeImplementations(m1, m2);
            }
        });
    
        [ViewController test1];
        [ViewController test2];
    }
    
    + (void)test1 {
        NSLog(@"test1");
    }
    
    + (void)test2 {
        NSLog(@"test2");
    }
    
    

    打印结果如下:

    2017-01-25 17:13:53.356 RuntimeDemo[16180:4689887] test1
    2017-01-25 17:13:53.356 RuntimeDemo[16180:4689887] test2
    
    

    居然交换失败了!

    然后我尝试着把代码挪到 + (void)load{} 里面:

    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            SEL s1 = @selector(test1);
            SEL s2 = @selector(test2);
            Class class = object_getClass((id)self);
            Method m1 = class_getClassMethod(class, s1);
            Method m2 = class_getClassMethod(class, s2);
            BOOL success = class_addMethod(class, s1, method_getImplementation(m2), method_getTypeEncoding(m2));
            if (success){
                class_replaceMethod(class, s2, method_getImplementation(m1), method_getTypeEncoding(m1));
            }
            else{
                method_exchangeImplementations(m1, m2);
            }
        });
    }
    
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        [ViewController test1];
        [ViewController test2];
    }
    
    

    打印结果如下:

    2017-01-25 17:23:32.739 RuntimeDemo[16212:4705811] test2
    2017-01-25 17:23:32.739 RuntimeDemo[16212:4705811] test1
    

    可以发现交换成功了!

    在找原因之前我们先来看一张经典的图:

    从左到右依次是:实例对象、类对象、元类(类对象的类)。

    我们通常这样来获取这三个对象:

    Person *obj = [Person new];
    NSLog(@"instance         :%p", obj);
    NSLog(@"class            :%p", object_getClass(obj));
    NSLog(@"meta class       :%p", object_getClass(object_getClass(obj)));
    
    

    我们来大胆猜想下:在实例方法中,self表示的是实例对象这个大家都知道,那在类(静态)方法中self表示的是不是就是实例对象的类,也就是类对象呢?我们直接撸代码来验证下:

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        [self test1];
        [ViewController test2];
    }
    
    - (void)test1 {
        NSLog(@"%p",self);
        NSLog(@"%p",object_getClass((id)self));
    }
    
    + (void)test2 {
        NSLog(@"%p",self);
    }
    
    

    打印结果如下:

    2017-01-25 22:54:37.325 RuntimeDemo[3841:159827] 0x7ff6ee50ac00
    2017-01-25 22:54:38.752 RuntimeDemo[3841:159827] 0x100ec0fe0
    2017-01-25 22:54:42.740 RuntimeDemo[3841:159827] 0x100ec0fe0
    

    果然不出所料,我们的大胆猜想是正确的。

    回过头,我们修改下实例方法中交换的实现:

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            ····
            
            Class class = object_getClass(object_getClass((id)self));
          
            ····
           }
        });
    
        [ViewController test1];
        [ViewController test2];
    }
    
    

    打印结果也自然而然的交换成功了:

    2017-01-25 23:02:12.579 RuntimeDemo[3926:164600] test2
    2017-01-25 23:02:12.580 RuntimeDemo[3926:164600] test1
    

    网上的 Method Swizzling 有两种写法,到底哪种靠谱?

    两种写法分别如下:

    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            SEL s1 = @selector(go);
            SEL s2 = @selector(stop);
            Class class = object_getClass((id)self);
            Method m1 = class_getClassMethod(class, s1);
            Method m2 = class_getClassMethod(class, s2);
            BOOL success = class_addMethod(class, s1, method_getImplementation(m2), method_getTypeEncoding(m2));
            if (success){
                class_replaceMethod(class, s2, method_getImplementation(m1), method_getTypeEncoding(m1));
            }
            else{
                method_exchangeImplementations(m1, m2);
            }
        });
    }
    
    
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            SEL s1 = @selector(go);
            SEL s2 = @selector(stop);
            Class class = object_getClass((id)self);
            Method m1 = class_getClassMethod(class, s1);
            Method m2 = class_getClassMethod(class, s2);
            method_exchangeImplementations(m1, m2);
    }
    
    

    第一种多了个判断,第二种是直接交换。

    文章一开始也说了,第一种是比较严谨的写法;而第二种,当我们想交换有多个继承关系的子类里面的方法并且子类没有实现父类的方法时,直接method_exchangeImplementations会把父类的方法也给交换了,一般这不是我们想要的结果,下面我们直接撸代码来验证下:

    父类 Person

    .h
    
    @interface Person : NSObject
    //静态(类) 方法
    + (void)go;
    + (void)stop;
    @end
    
    .m
    
    @implementation Person
    
    + (void)go {
        NSLog(@"Go!");
    }
    
    + (void)stop {
        NSLog(@"Stop!");
    }
    @end
    
    

    子类 Programmer

    .h
    
    #import "Person.h"
    
    @interface Programmer : Person
    
    @end
    
    .m
    
    #import "Programmer.h"
    #import <objc/runtime.h>
    
    @implementation Programmer
    
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            SEL s1 = @selector(go);
            SEL s2 = @selector(stop);
            Class class = object_getClass((id)self);
            Method m1 = class_getClassMethod(class, s1);
            Method m2 = class_getClassMethod(class, s2);
            method_exchangeImplementations(m1, m2);
        });
    }
    
    
    //+ (void)go {
    //    NSLog(@"Programmer - go");
    //}
    //
    //+ (void)stop {
    //    NSLog(@"Programmer - stop");
    //}
    @end
    
    

    调用

    - (void)viewDidLoad {
        [super viewDidLoad];
        
        [Person go];
        [Person stop];
        NSLog(@"---------");
        [Programmer go];
        [Programmer stop];
        
    }
    
    

    首先,我们只实现父类的 gostop 这两个静态方法,打印如下:

    2017-01-25 23:30:12.850 RuntimeDemo[4180:180408] Stop!
    2017-01-25 23:30:12.851 RuntimeDemo[4180:180408] Go!
    2017-01-25 23:30:12.851 RuntimeDemo[4180:180408] ---------
    2017-01-25 23:30:12.851 RuntimeDemo[4180:180408] Stop!
    2017-01-25 23:30:12.852 RuntimeDemo[4180:180408] Go!
    

    可以发现,子类Programmer没有实现父类的方法直接交换时, 父类Person的方法也被交换了!

    我们接着打开子类的 gostop 这两个静态方法,打印如下:

    2017-01-25 23:32:22.953 RuntimeDemo[4215:181987] Go!
    2017-01-25 23:32:22.954 RuntimeDemo[4215:181987] Stop!
    2017-01-25 23:32:22.956 RuntimeDemo[4215:181987] ---------
    2017-01-25 23:32:22.956 RuntimeDemo[4215:181987] Programmer - stop
    2017-01-25 23:32:22.956 RuntimeDemo[4215:181987] Programmer - go
    
    

    可以发现,子类实现了父类的方法,直接交换也是没有问题的!

    我们加上判断:

    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            ...
            
            BOOL success =  class_addMethod(class, s1, method_getImplementation(m2), method_getTypeEncoding(m2));
            if (success){
                class_replaceMethod(class, s2, method_getImplementation(m1), method_getTypeEncoding(m1));
            }else{
                method_exchangeImplementations(m1, m2);
            }
            
            ...
        });
    }
    
    

    把上面两种情况在试验下,可以发现交换的仅仅是子类Programmer的方法,父类Person是没有被交换的!大家可以自己尝试下。

    稍稍的解释下:

    class_addMethod函数会检查方法有没有实现,如果已经实现会返回 NO ,也就是直接走method_exchangeImplementations方法;没有实现会先在当前类增加一个新的实现方法,再把目标类中的方法通过class_replaceMethod函数替换为旧有的实现;

    结尾

    以上就是我遇到的问题,希望对大家能有点帮助。最后也希望大家能够亲自动手敲一遍,加深下印象。

    参考链接

    http://ios.jobbole.com/91962/

    http://ios.jobbole.com/81657/

    http://blog.csdn.net/horkychen/article/details/8532087

    相关文章

      网友评论

      • Noah1985:这个一直都是知道的,但也属于深层记忆部分,如果突然被问起,也不一定能答得正确。毕竟用的也少,突然看到这篇文章后又唤起了这些记忆。当然。。。没过多久估计又得忘了。当然,如果真正到自己遇到问题后才能将这些深层记忆为己所用。
        ifelseboyxx:是的,我写的目的也是加深下印象,方便随时查看:smile:

      本文标题:浅谈 Method Swizzling 中遇到的一些问题

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