iOS底层学习——Method Swizzling方法交换

2,124 阅读4分钟

1.方法交换的原理

每一个继承于NSObject的类都能自动获得runtime的支持。在这样的一个类中,有一个isa指针,指向该类定义的数据结构体,这个结构体是由编译器编译时为类创建的。在这个结构体中又包括了,指向其父类类定义的指针以及Dispatch tableDispatch table是一张SELIMP的对应表。

也就是说方法编号SEL最后还是要通过Dispatch table表寻找到对应的IMPIMP就是一个函数指针,然后执行这个方法。

  • 方法编号SEL方法实现IMP的对应关系

    image.png

  • 方法交换后对应关系

    image.png

    • oriSEL的方法实现变成了swiIMP
    • swiSEL的方法实现变成了oriIMP

    也就是调用oriSEL方法,最终方法实现是swiIMP

  • 方法交换的方式

        // 类中获取oriSEL对应的方法实现
        Method oriMethod = class_getInstanceMethod(cls, oriSEL);
        // 获取swiSEL对应的方法实现
        Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
        // 将两个方法实现进行交换,
        method_exchangeImplementations(oriMethod, swiMethod);
    

    在进行方法交换操作时,建议放在单例下进行,避免重复调用导致交换了个寂寞。通过上面的方法可以理解,交换的是两者的方法实现。

2.方法交换案例分析

1.递归问题分析

  • 案例描述

    创建一个LGStudent类,类中有两个实例方法,lg_studentInstanceMethodstudentInstanceMethod,在load方法中对两个方法进行交换,同时,lg_studentInstanceMethod的实现中再次调用lg_studentInstanceMethod方法。

  • 实现代码

        @implementation ViewController
        - (void)viewDidLoad {
            [super viewDidLoad];
    
            LGStudent *s = [[LGStudent alloc] init];
            [s studentInstanceMethod];
        @end
    
        @implementation LGStudent
    
        + (void)load{
            static dispatch_once_t onceToken;
            dispatch_once(&onceToken, ^{
                NSLog(@"方法交换---:%s", __func__);
                Method oriIMP = class_getInstanceMethod(self, @selector(studentInstanceMethod));
                Method swiIMP = class_getInstanceMethod(self, @selector(lg_studentInstanceMethod));
                method_exchangeImplementations(oriIMP, swiIMP);
            });
        }
    
        // 是否递归
        - (void)lg_studentInstanceMethod{
            [self lg_studentInstanceMethod];
            NSLog(@"LGStudent对象方法:%s", __func__);
        }
    
        - (void)studentInstanceMethod{
            NSLog(@"LGStudent对类方法:%s", __func__);
        }
        @end
    
  • 问题分析

    lg_studentInstanceMethod中再次调用该方法,是否会引起递归调用呢?

    运行程序看看结果,见下图:

    image.png

    分析:并没有引起递归,因为进行了方法交换,所以调用对象方法studentInstanceMethod,会找到lg_studentInstanceMethod的方法实现,而lg_studentInstanceMethod中有调用lg_studentInstanceMethod,而此时它的方法实现已经指向了studentInstanceMethod。见下图:

    image.png

2.交换父类的方法

  • 案例描述

    创建一个LGStudent类,类中有一个实例方法,lg_studentInstanceMethod,其父类LGPerson中有一个实例方法personInstanceMethod,在LGStudent类的load方法中对进行方法交换,将lg_studentInstanceMethod方法交换成父类中的personInstanceMethod方法。

  • 实现代码

        @implementation ViewController
        - (void)viewDidLoad {
            [super viewDidLoad];
    
            LGStudent *s = [[LGStudent alloc] init];
            // 调用父类中的方法
            [s personInstanceMethod];
        @end
        
         // LGPerson是父类
        @implementation LGPerson
        
        - (void)personInstanceMethod{
            NSLog(@"person对象方法:%s",__func__);
        }
    
        @end
    
        // LGStudent是子类
        @implementation LGStudent
    
        + (void)load{
            static dispatch_once_t onceToken;
            dispatch_once(&onceToken, ^{
                NSLog(@"方法交换---:%s", __func__);
                Method oriIMP = class_getInstanceMethod(self, @selector(personInstanceMethod));
                Method swiIMP = class_getInstanceMethod(self, @selector(lg_studentInstanceMethod));
                method_exchangeImplementations(oriIMP, swiIMP);
            });
        }
    
        // 是否递归
        - (void)lg_studentInstanceMethod{
            [self lg_studentInstanceMethod];
            NSLog(@"LGStudent对象方法:%s", __func__);
        }
    
        @end
    
  • 问题分析

    LGStudent对象是否能够成功调用personInstanceMethod方法?

    运行程序看看结果,见下图:

    image.png

    分析:成功调用,因为子类对象调用父类方法personInstanceMethod,我们在学习消息发送的原理时已经知道,其会进行慢速方法查找找到父类方法。但是此时父类方法对应的方法实现已经被交换成了,子类的lg_studentInstanceMethod方法,所有会执行子类的lg_studentInstanceMethod方法实现。于此同时子类中调用lg_studentInstanceMethod方法,最终的方法实现是父类的personInstanceMethod方法。

  • 案例扩展

    如果调用父类对象的personInstanceMethod方法会怎样呢?

    运行程序看看结果,见下图:

    image.png

    分析:报错,原因是什么呢,首先父类调换用personInstanceMethod方法会执行子类中的lg_studentInstanceMethod实现,但是但是此时又调用lg_studentInstanceMethod方法,而此时的调用者是LGPerson对象,父类中并没有lg_studentInstanceMethod方法的实现。所以方法找不到,进而报错。

    image.png

    分析在开发中,如果进行方法交换,一定要确保方法已经实现,否则会出现本例中啃爹的现象(方法交换,而父类没有方法的实现,导致报错)。所以在进行相关方法交换时,尽量避免涉及到其父类或者其子类的方法。

3.方法交换设计思路

通过上面的案例,为避免上面案例的问题,总结以下实现思路:

+ (void)lg_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{

    if (!cls) NSLog(@"传入的交换类不能为空");
    
    // 获取类中的方法
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    // 要被交换的方法
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    
    // 判断类中是否存在该方法-避免动作没有意义
    if (!oriMethod) { 

        // 在oriMethod为nil时,添加oriSEL的方法,实现为swiMethod
        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));

        // 替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){

            NSLog(@"来了一个空的 imp");
        }));
    }

    // 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
    // 交换自己没有实现的方法:
    //   首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
    //   然后再将父类的IMP给swizzle  personInstanceMethod(imp) -> swizzledSEL
    //oriSEL:personInstanceMethod

    // 向类中添加oriSEL方法,方法实现为swiMethod
    BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    
    // 自己有意味添加方法失败-所以这里会是false
    if (didAddMethod) {
        // 如果添加成功,表示原本没有oriMethod方法,此时将swizzledSEL的方法实现,替换成oriMethod实现
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
        // 方法交换
        method_exchangeImplementations(oriMethod, swiMethod);
    }
}