iOS 方法交换 Method Swizzling

作者: 木扬音 | 来源:发表于2021-05-28 22:11 被阅读0次

    什么是Method Swizzling

    • Method Swizzling 就是方法交换,俗称黑魔法,主要是在运行时将一个方法的实现替换成另外一个方法的实现
    • 在OC中我们可以利用Method Swizzling来实现AOP(切面编程)
      • OOP:面向对象编程-->针对业务处理过程的实体及其属性和行为进行抽象封装,划分出更加清晰的逻辑单元
      • AOP:面向切面编程-->针对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。主要的功能是:日志记录,性能统计,安全控制,事务处理,异常处理等等。
    • 每个类都有自己的一个方法列表,即methodList,每个方法中都包含了方法的selimp,方法交换就是将原来sel和imp的关系断开,将sel和新的imp建立新的关系
      方法交换原理

    常用的API

    • class_getInstanceMethod:获取实例方法
    • class_getClassMethod :获取类方法
    • method_getImplementation:获取一个方法的实现
    • method_setImplementation:设置一个方法的实现
    • method_getTypeEncoding:获取方法实现的编码类型
    • class_addMethod:添加方法
    • class_replaceMethod:替换方法,用一个方法的实现,替换另一个方法的实现,即aIMP 指向 bIMP,但是bIMP不一定指向aIMP
    • method_exchangeImplementations:交换两个方法的实现,即 aIMP -> bIMP, bIMP -> aIMP

    【问题1】method-swizzling使用过程中重复交换

    假如在load中实现了method-swizzling,由于load方法的多次调用,这样会导致方法的重复交换,使方法的sel又重新指向原来的imp

    解决方案

    通过单例来确保方法只被交换一次

    + (void)load{
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [LGRuntimeTool lg_bestMethodSwizzlingWithClass:self oriSEL:@selector(helloword) swizzledSEL:@selector(lg_studentInstanceMethod)];
        });
    }
    

    【问题2】交换的方法子类没有实现,而父类实现了

    运行下面这段代码会出现什么问题?

    //*********LGPerson类*********
    @interface LGPerson : NSObject
    - (void)personInstanceMethod;
    @end
    
    @implementation LGPerson
    - (void)personInstanceMethod{
        NSLog(@"person对象方法:%s",__func__);  
    }
    @end
    
    //*********LGStudent类*********
    @interface LGStudent : LGPerson
    - (void)helloword;
    + (void)sayHello;
    @end
    
    @implementation LGStudent
    @end
    
    //*********调用*********
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        // 黑魔法坑点二: 子类没有实现 - 父类实现
        LGStudent *s = [[LGStudent alloc] init];
        [s personInstanceMethod];
        
        LGPerson *p = [[LGPerson alloc] init];
        [p personInstanceMethod];
    }
    

    我们通过分类来实现方法交换,和封装好的方法交换类

    @implementation LGStudent (LG)
    
    + (void)load{
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            [LGRuntimeTool lg_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
        });
    }
    
    // personInstanceMethod 我需要父类的这个方法的一些东西
    // 给你加一个personInstanceMethod 方法
    // imp
    
    - (void)lg_studentInstanceMethod{
        ////是否会产生递归?--不会产生递归,原因是lg_studentInstanceMethod 会走 oriIMP,即personInstanceMethod的实现中去
        [self lg_studentInstanceMethod];
        NSLog(@"LGStudent分类添加的lg对象方法:%s",__func__);
    }
    
    @end
    
    //封装好的方法交换类
    @implementation LGRuntimeTool
    + (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
        
        if (!cls) NSLog(@"传入的交换类不能为空");
    
        Method oriMethod = class_getInstanceMethod(cls, oriSEL);
        Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
        method_exchangeImplementations(oriMethod, swiMethod);
    }
    

    运行上面代码,会发现在子类调用personInstanceMethod方法时会崩溃

    image.png
    • LGStudent子类调用不报错是因为:student中的imp交换成了lg_studentInstanceMethod,在LGStudent分类中有lg_studentInstanceMethod的实现,所以不会奔溃

    • LGPerson父类调用报错是因为:我们是在LGStudent分类中进行的方法交换,将personimp交换成了LGStudent的lg_studentInstanceMethod,这样我们就需要去LGPerson的方法列表中去查找lg_studentInstanceMethod,但是LGPerson没有该方法(imp找不到)奔溃

    解决方案

    我们可以通过class_addMethod尝试添加你要交换的方法,来判断当前类是否有这样方法

    • 添加成功--类中没有这个方法,通过class_replaceMethod进行方法替换,因为class_replaceMethod内部会自动调用class_addMethod进行添加
    • 添加失败--类中有这个方法,通过method_exchangeImplementations进行方法交换
    + (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
        
        if (!cls) NSLog(@"传入的交换类不能为空");
        
        Method oriMethod = class_getInstanceMethod(cls, oriSEL);
        Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
       
        // 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
        // 交换自己没有实现的方法:
        //   首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
        //   然后再将父类的IMP给swizzle  personInstanceMethod(imp) -> swizzledSEL 
    
        BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
    
        if (success) {// 自己没有 - 交换 - 没有父类进行处理 (重写一个)
            class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        }else{ // 自己有
            method_exchangeImplementations(oriMethod, swiMethod);
        }   
    }
    

    下面是class_replaceMethodclass_addMethodmethod_exchangeImplementations的源码实现

    image.png

    其中class_replaceMethodclass_addMethod中都调用了addMethod方法,区别在于bool值的判断,下面是addMethod的源码实现

    image.png

    【问题3】:子类和父类都没有实现改方法,下面的调用有什么问题?

    //*********LGPerson类*********
    @interface LGPerson : NSObject
    - (void)personInstanceMethod;
    @end
    
    @implementation LGPerson
    @end
    
    //*********LGStudent类*********
    @interface LGStudent : LGPerson
    - (void)helloword;
    + (void)sayHello;
    @end
    
    @implementation LGStudent
    @end
    
    //*********调用*********
    - (void)viewDidLoad {
        [super viewDidLoad];
    
        // 黑魔法坑点二: 子类没有实现 - 父类实现
        LGStudent *s = [[LGStudent alloc] init];
        [s personInstanceMethod];
        
        LGPerson *p = [[LGPerson alloc] init];
        [p personInstanceMethod];
    }
    

    运行奔溃


    image.png

    原因是递归死循环导致栈溢出---- 由于personInstanceMethod方法没有实现,在进行方法交换的时候找不到oriMethod,导致交换方法失败,当我们调用personInstanceMethod(oriMethod)方法时,oriMethod会进入到分类中的lg_studentInstanceMethod方法,然而lg_studentInstanceMethod方法中又调用了lg_studentInstanceMethod,但是现在lg_studentInstanceMethod并没有指向oriMethod,导致自己调用自己直接递归死循环

    解决方案
    • 通过class_addMethodoriSEL添加swiMethod 方法
    • 通过method_setImplementationswiMethodIMP指向空实现的方法
    + (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时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
            class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
            method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
        }
        
        // 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
        // 交换自己没有实现的方法:
        //   首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
        //   然后再将父类的IMP给swizzle  personInstanceMethod(imp) -> swizzledSEL
        //oriSEL:personInstanceMethod
    
        BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        if (didAddMethod) {
            class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        }else{
            method_exchangeImplementations(oriMethod, swiMethod);
        }
        
    }
    

    method-swizzling --- 类方法

    在分类的load方法中实现类方法的方法交换

    + (void)load{
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
             [LGRuntimeTool lg_bestClassMethodSwizzlingWithClass:self oriSEL:@selector(sayHello) swizzledSEL:@selector(lg_studentClassMethod)];
        });
    }
    + (void)lg_studentClassMethod{
        NSLog(@"LGStudent分类添加的lg类方法:%s",__func__);
       [[self class] lg_studentClassMethod];
    }
    
    • 通过class_getClassMethod获取类方法
    • 调用class_addMethodclass_replaceMethod方法添加和替换时,需要传入的类是元类,元类可以通过object_getClass方法获取类的元类
    //封装的method-swizzling方法
    + (void)lg_bestClassMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
        
        if (!cls) NSLog(@"传入的交换类不能为空");
    
        Method oriMethod = class_getClassMethod([cls class], oriSEL);
        Method swiMethod = class_getClassMethod([cls class], swizzledSEL);
        
        if (!oriMethod) { // 避免动作没有意义
            // 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
            class_addMethod(object_getClass(cls), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
            method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
                NSLog(@"来了一个空的 imp");
            }));
        }
        
        // 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
        // 交换自己没有实现的方法:
        //   首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
        //   然后再将父类的IMP给swizzle  personInstanceMethod(imp) -> swizzledSEL
        //oriSEL:personInstanceMethod
    
        BOOL didAddMethod = class_addMethod(object_getClass(cls), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        
        if (didAddMethod) {
            class_replaceMethod(object_getClass(cls), swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        }else{
            method_exchangeImplementations(oriMethod, swiMethod);
        }
        
    }
    

    Method Swizzling的应用

    最常用的应用是防止数组、字典等越界崩溃

    在iOS 中NSNumberNSArrayNSDictionary这些类都是类簇,例如NSArray的实现是由多个类组成的,所有如果想对NSArray进行方法交换,必须获取到其“真身”进行Swizzling,直接对NSArray进行操作是无效的

    image.png
    NSArray方法交换

    创建NSArray的一个分类YP

    @implementation NSArray (YP)
    //如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效
    + (void)load{
        Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
        Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(yp_objectAtIndex:));
        
        method_exchangeImplementations(fromMethod, toMethod);
    }
    
    //如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效
    - (id)yp_objectAtIndex:(NSUInteger)index{
        //判断下标是否越界,如果越界就进入异常拦截
        if (self.count-1 < index) {
            // 这里做一下异常处理,不然都不知道出错了。
    #ifdef DEBUG  // 调试阶段
            return [self yp_objectAtIndex:index];
    #else // 发布阶段
            @try {
                return [self yp_objectAtIndex:index];
            } @catch (NSException *exception) {
                // 在崩溃后会打印崩溃信息,方便我们调试。
                NSLog(@"---------- %s Crash Because Method %s  ----------\n", class_getName(self.class), __func__);
                NSLog(@"%@", [exception callStackSymbols]);
                return nil;
            } @finally {
                
            }
    #endif
        }else{ // 如果没有问题,则正常进行方法调用
            return [self yp_objectAtIndex:index];
        }
    }
    
    @end
    

    相关文章

      网友评论

        本文标题:iOS 方法交换 Method Swizzling

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