写点什么

Method-Swizzling 方法交换

  • 2022 年 3 月 08 日
  • 本文字数:6426 字

    阅读完需:约 21 分钟

method-swizzling 是什么?

  • method-swizzling的含义是方法交换,其主要作用是在运行时将一个方法的实现替换成另一个方法的实现,这就是我们常说的iOS黑魔法

  • 在 OC 中就是利用method-swizzling实现AOP,其中AOP(Aspect Oriented Programming,面向切面编程)是一种编程的思想,区别于OOP(面向对象编程)

    OOP 和 AOP 都是一种编程的思想

    OOP编程思想更加倾向于对业务模块的封装,划分出更加清晰的逻辑单元;

    AOP面向切面进行提取封装,提取各个模块中的公共部分,提高模块的复用率,降低业务之间的耦合性

  • 每个类都维护着一个方法列表,即methodListmethodList中有不同的方法Method,每个方法中包含了方法的selIMP,方法交换就是将sel和imp原本的对应断开,并将sel和新的IMP生成对应关系

如下图所示,交换前后的 sel 和 IMP 的对应关系



method-swizzling 涉及的相关 API

  • 通过sel获取方法Method

    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 使用过程中的一次性问题

所谓的一次性就是:mehod-swizzling写在load方法中,而load方法会主动调用多次,这样会导致方法的重复交换,使方法 sel 的指向又恢复成原来的 imp 的问题

解决方案

可以通过单例设计原则,使方法交换只执行一次,在 OC 中可以通过dispatch_once实现单例

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

坑点 2:子类没有实现,父类实现了

在下面这段代码中,LGPerson中实现了personInstanceMethod,而LGStudent继承自LGPerson,没有实现personInstanceMethod,运行下面这段代码会出现什么问题?

//*********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];}复制代码
复制代码

其中,方法交换代码如下,是通过LGStudent的分类LG实现

@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复制代码
复制代码

下面是封装好的method-swizzling方法

@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);}复制代码
复制代码

通过实际代码的调试,发现会在 p 调用personInstanceMethod方法时崩溃,下面来进行详细说明



  • [s personInstanceMethod];中不报错是因为 student中的imp交换成了lg_studentInstanceMethod,而LGStudent中有这个方法(在 LG 分类中),所以不会报错

  • 崩溃的点在于[p personInstanceMethod];,其本质原因:LGStudent的分类LG中进行了方法交换,将personimp 交换成了 LGStudent中的lg_studentInstanceMethod,然后需要去 LGPerson中的找lg_studentInstanceMethod,但是LGPerson中没有lg_studentInstanceMethod方法,即相关的imp找不到,所以就崩溃了

优化:避免 imp 找不到

通过class_addMethod尝试添加你要交换的方法

  • 如果添加成功,即类中没有这个方法,则通过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的源码实现


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


坑点 3:子类没有实现,父类也没有实现,下面的调用有什么问题?

在调用personInstanceMethod方法时,父类 LGPerson 中只有声明,没有实现,子类 LGStudent 中既没有声明,也没有实现

//*********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];}复制代码
复制代码

经过调试,发现运行代码会崩溃,报错结果如下所示 

 .

原因是 栈溢出递归死循环了,那么为什么会发生递归呢?----主要是因为 personInstanceMethod没有实现,然后在方法交换时,始终都找不到 oriMethod,然后交换了寂寞,即交换失败,当我们调用personInstanceMethod(oriMethod)时,也就是oriMethod会进入 LG 中lg_studentInstanceMethod方法,然后这个方法中又调用了lg_studentInstanceMethod,此时的lg_studentInstanceMethod并没有指向oriMethod ,然后导致了自己调自己,即递归死循环

优化:避免递归死循环

  • 如果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 - 类方法

类方法和实例方法的 method-swizzling 的原理是类似的,唯一的区别是类方法存在元类中,所以可以做如下操作

  • LGStudent中只有类方法sayHello的声明,没有实现

@interface LGStudent : LGPerson- (void)helloword;+ (void)sayHello;@end
@implementation LGStudent
@end复制代码
复制代码


  • 在 LGStudent 的分类的 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); } }复制代码
复制代码


  • 调用如下

- (void)viewDidLoad {    [super viewDidLoad];        [LGStudent sayHello];}复制代码
复制代码
  • 运行结果如下,由于符合方法没有实现,所以会走到空的imp



method-swizzling 的应用

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

在 iOS 中NSNumberNSArrayNSDictionary等这些类都是类簇,一个NSArray的实现可能由多个类组成。所以如果想对 NSArray 进行 Swizzling,必须获取到其“真身”进行Swizzling,直接对NSArray进行操作是无效的

下面列举了 NSArray 和 NSDictionary 本类的类名,可以通过 Runtime 函数取出本类。


以 NSArray 为例

  • 创建 NSArray 的一个分类CJLArray

@implementation NSArray (CJLArray)//如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效+ (void)load{    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));    Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(cjl_objectAtIndex:));        method_exchangeImplementations(fromMethod, toMethod);}
//如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效- (id)cjl_objectAtIndex:(NSUInteger)index{ //判断下标是否越界,如果越界就进入异常拦截 if (self.count-1 < index) { // 这里做一下异常处理,不然都不知道出错了。#ifdef DEBUG // 调试阶段 return [self cjl_objectAtIndex:index];#else // 发布阶段 @try { return [self cjl_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 cjl_objectAtIndex:index]; }}
@end复制代码
复制代码


  • 测试代码

 NSArray *array = @[@"1", @"2", @"3"];[array objectAtIndex:3];复制代码
复制代码
  • 打印结果如下,会输出崩溃的日志,但是实际并不会崩溃



最后

如果你觉得此文对你有一丁点帮助,点个赞。或者可以加入我的开发交流群:1025263163 相互学习,我们会有专业的技术答疑解惑

如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点 star:http://github.crmeb.net/u/defu不胜感激 !

PHP 学习手册:https://doc.crmeb.com

技术交流论坛:https://q.crmeb.com

用户头像

还未添加个人签名 2021.11.02 加入

CRMEB就是客户关系管理+营销电商系统实现公众号端、微信小程序端、H5端、APP、PC端用户账号同步,能够快速积累客户、会员数据分析、智能转化客户、有效提高销售、会员维护、网络营销的一款企业应用

评论

发布
暂无评论
Method-Swizzling 方法交换_💥 玩命不玩心💥_InfoQ写作平台