iOS Method Swizzling方法交换

前言

在平时开发过程中我们好多时候都需要进行方法的交换,就会用到Method Swizzling方法,但是用的时候我们知道它的原理是什么嘛?那么以下就进行详细的分析。

方法交换的原理

iOS中每一个继承于NSObject的类都能自动获得runtime的支持。在这样的一个类中,有一个isa指针,指向该类定义的数据结构体,这个结构体是由编译器编译时为类创建的。在这个结构体中又包括了,指向其父类类定义的指针以及Dispatch tableDispatch table是一张SELIMP的对应表。
方法编号SEL最后还是要通过Dispatch table表寻找到对应的IMPIMP就是一个函数指针,然后执行这个方法。

  • 方法编号SEL和方法实现IMP的对应关系
    SEL与IMP对应关系
  • 方法交换后对应关系
    交换后SEL与IMP对应关系
  • selectorC的方法实现变成了IMPn
  • selectorN的方法实现变成了IMPc

最后调用selectorC方法实现selectorN方法,调用selectorN方法实现了selectorC方法,从而实现了方法的交换

方法交换的方式

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

注意:在进行方法交换操作时,建议放在单例下进行,避免重复调用导致没效果,因为两者再交换一次的换就会 换回原来的IMP

方法交换案例分析

递归问题分析

创建一个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

以上代码会出现递归的现象吗?那么我们运行代码看看:

代码运行结果

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

交换父类的方法

创建一个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方法?那么我们运行代码看看咯,请往下看:

运行结果

根据以上的运行结果明显是可以调用成功没有任何的问题!!
因为子类对象调用父类方法personInstanceMethod,根据消息发送的原理时已经知道,其会进行慢速方法查找找到父类方法。但是此时父类方法对应的方法实现已经被交换成了,子类的lg_studentInstanceMethod方法,所有会执行子类的lg_studentInstanceMethod方法实现。于此同时子类中调用lg_studentInstanceMethod方法,最终的方法实现是父类的personInstanceMethod方法。

拓展:父类调用personInstanceMethod会出现什么情况呢?
修改代码,运行程序得到下面的结果:

运行结果

根据以上的运行结果,可以看到是不成功的!!
因为经过两个方法的交换,在父类调用personInstanceMethod方法的时候,会执行子类中的lg_studentInstanceMethod实现,但是但是此时又调用lg_studentInstanceMethod方法,而此时的调用者是LGPerson对象,父类中并没有lg_studentInstanceMethod方法的实现。消息查找是不能父类找子类的,所以就报找不到方法的错误出来。

注意:在开发过程中我们一定要注意方法是否已经实现,不然的话就会出的先案例中的崩溃情况(方法交换,而父类没有方法的实现,导致报错),在方法交换的时候尽量避免涉及到子类根父类的方法!!

方法交换设计思路

通过上面案例的分析总结出以下的方法交换的设计思路:

+ (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);
    }
}

你可能感兴趣的:(iOS Method Swizzling方法交换)