OC基础知识点之-Method Swizzling黑魔法(方法交换)

4,403 阅读7分钟

系列文章:OC底层原理系列OC基础知识系列 从这章开始我们将进入OC基础知识点的研究探索,OC底层原理部分暂时告于段落(后续有新的底层内容再做补充)往期文章汇总:OC底层原理系列

这篇文章我们来讲一下我们开发使用到方法交换(Method Swizzling)

方法交换(Method Swizzling)基础

Method Swizzling定义

  • method-swizzling的含义是方法交换,其主要作用是在运行时将一个方法的实现替换成另一个方法的实现,这就是我们常说的iOS黑魔法
  • 在OC中可以通过method-swizzling实现AOP编程(Aspect Oriented Programming,面向切面编程),AOP区别去OOP(面向对象编程)
  • 每个类都有自己的方法列表,即methodList,methodList里有不同的方法即Method,每个方法中包含了方法的sel和IMP,方法交换就是将sel和imp原本的对应断开,并将sel和新的IMP生成对应关系。 如下图所示对应关系:

Method Swizzling涉及的相关方法

  • class_getInstanceMethod:获取实例方法
  • class_getClassMethod:获取类方法
  • method_getImplementation:获取方法实现
  • method_setImplementation:设置方法实现
  • method_getTypeEncoding:获取函数的编码,其结果是一串值。
  • class_addMethod:添加方法实现
  • class_replaceMethod:用一个方法实现去替换另一个方法实现,即AImp指向BImp,但是BImp不一定指向AImp。
  • method_exchangeImplementations:方法交换(上面是替换)即AImp指向BImp,BImp指向AImp。

上面我们对Method Swizzling有个基本了解,下面我们就来看下Method Swizzling在项目使用过程中会出现哪些问题

Method Swizzling使用

子类方法没有实现,父类实现了

我们写如下代码

**********ViewController**********
// ViewController.m
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    LGStudent *s = [[LGStudent alloc] init];
    [s personInstanceMethod];
    LGPerson *p = [[LGPerson alloc] init];
    [p personInstanceMethod];
}
@end

**********LGPerson**********
// LGPerson.h
@interface LGPerson : NSObject
- (void)personInstanceMethod;
+ (void)personClassMethod;
@end
// LGPerson.m
@implementation LGPerson
- (void)personInstanceMethod{
    NSLog(@"person对象方法:%s",__func__);
}
+ (void)personClassMethod{
    NSLog(@"person类方法:%s",__func__);
}
@end

**********LGStudent**********
// LGStudent.h
@interface LGStudent : LGPerson
- (void)helloword;
@end
@implementation LGStudent

@end

**********LGStudent+LG**********
// LGStudent+LG.h
@interface LGStudent (LG)

@end
// LGStudent+LG.m
@implementation LGStudent (LG)
+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [LGRuntimeTool lg_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
    });
}

- (void)lg_studentInstanceMethod{
    [self lg_studentInstanceMethod]; 
    NSLog(@"LGStudent分类添加的lg对象方法:%s",__func__);
}
@end

**********LGRuntimeTool**********
@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);
}
@end

上面代码我们可以看到personInstanceMethod方法声明实现在LGPerson,Student的父类是LGPerson。lg_methodSwizzlingWithClass此时的方法用的是方法交换,也就是上面解释的AImp指向BImp,同时BImp指向AImp。

分析:我们之所以将交换方法写在+load方法里,是因为我们知道load方法的调用会早于main函数,系统会自动帮我们引入,不需要我们自己引入分类,系统会直接帮我们交换,非常的省时省力,而且放到load方法里能够及时的封装,不被外界看到。之所以用单例的原因是为了防止load方法重复调用,导致方法交换重复进行,失去方法交换的意义。但是load方法会阻碍启动,所以有时候会将方法写initialize方法里。

但是我们看到lg_studentInstanceMethod方法里调用了lg_studentInstanceMethod方法,那么此时会不会产生递归呢?答案是不会的。原因:我们在load方法里将lg_studentInstanceMethod和personInstanceMethod进行了方法交换那么[self lg_studentInstanceMethod]就会调用personInstanceMethod方法实现,所以并不会产生递归。

下面我们运行代码,发现报错 上图我们知道报错在[p lg_studentInstanceMethod]上,原因是我们已经将personInstanceMethod交换成lg_studentInstanceMethod,此时调用personInstanceMethod其实调用的是lg_studentInstanceMethod方法,由于LGStudent分类实现了该方法,所以没问题。但是LGPerson并没有实现lg_studentInstanceMethod方法,所以会报错

优化方法交换(处理imp找不到情况)

下面我们对代码做如下处理

[LGRuntimeTool lg_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
替换成
[LGRuntimeTool lg_betterMethodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];

// lg_betterMethodSwizzlingWithClass方法实现
+ (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 
    //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);
    }
}

再运行,我们发现就没有报错,这是为什么?

// 这是主要因为lg_methodSwizzlingWithClass用的是method_exchangeImplementations,而lg_betterMethodSwizzlingWithClass是下面方法

BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (success) {
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
    method_exchangeImplementations(oriMethod, swiMethod);
}

我们查看下class_replaceMethod、class_addMethod、method_exchangeImplementations源码 我们发现class_replaceMethod以及class_addMethod都调用了addMethod方法,不同的是class_replaceMethod取addMethod相同值,class_addMethod取addMethod反向值,下面我们看下addMethod的方法实现:

  • 5695行:检查是否已是知类
  • 6602-6607行:已存在,6604行-如果replace为false就获取m的imp,6606行-如果replace为true,就设置方法实现。
  • 6609-6625行:不存在,6609行-对rwe进行初始化,6612-6619行-添加方法,6623行-刷新所有缓存

addMethod方法就是查看如果cls没有交换的方法,就将这个方法加入methods中并返回false,如果自己实现了,就返回true。但是它的结果是取反

上面通过源码我们知道方法didAddMethod是判断swiMethod方法是否存在,下面判断如果没有实现,则进行替换,然后父类进行处理,如果实现了就直接进行交换 这两个方法不同是一个是交换,一个是替换,当自己没有的时候就是替换,自己有的时候就进行的是交换

继续优化(父类没有实现,子类也没有实现)

上面说了替换和交换问题,下面我们再看下一个问题,Person类不实现personInstanceMethod会怎么样。我们运行代码 我们发现报错,而且出现递归导致内存溢出。原因是personInstanceMethod没有实现method_exchangeImplementations这个方法交换是失败了,就没有实现,所以方法自己调自己,出现递归。 下面我们改下交换方法

将[LGRuntimeTool lg_betterMethodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
替换成
[LGRuntimeTool lg_bestMethodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];

********lg_bestMethodSwizzlingWithClass*********实现
+ (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){
            NSLog(@"空的imp");
        }));
    }
    // 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
    // 交换自己没有实现的方法:
    //   首先第一步:会先尝试给自己添加要交换的方法 :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);
    }
}

这个方法就是发现交换方法为空,我们就指定操作,我们运行下代码 方法lg_bestMethodSwizzlingWithClass对交换的方法进行判断,就是防止出现死循环

  • 如果oriMethod为空,为了避免交换方法没有意义,需要做些事情
    • 通过class_addMethod方法给oriSEL添加swiMethod方法
    • 通过method_setImplementation将swiMethod的imp指向我们指定的实现,在此我们打印了空的imp字符串。

总结

通过上面的解释我们看到三种不同的方法交换

  • 第一种就是替换method_exchangeImplementations就回导致如果替换方法类里未实现就回报错
  • 第二种是如果有方法未实现就进行交换class_replaceMethod将自己未实现的方法进行交换,实现的方法进行替换method_exchangeImplementations。这会导致如果自己父类也没有实现就可能(方法里面调用该方法)会导致出现递归循环,报错
  • 第三种如果方法没有实现将未实现的方法替换后将方法复制一个不做任何事的空实现。这个是最好的解决方案

写到最后

method-swizzling最常用的应用是防止数组越界崩溃,防止字典赋值出现nil崩溃 我们看下NSMutableDictionary赋值防崩溃 我们先不进行方法交换,去给dic赋值nil会报错 下面我们方法交换打开,再次给nil值,再运行 没有报错,数组部分大家可以尝试写一下