当前位置: 首页 > news >正文

「iOS」黑魔法——方法交换

黑魔法

  • 【iOS】方法交换(Method-Swizzling)
    • 一、什么是 Method-Swizzling
      • 原理图示
    • 二、相关 API
    • 三、方法交换的风险
      • 风险 1:多次交换导致逻辑混乱
      • 风险 2:跨类交换引发崩溃
          • 要在当前类的方法中进行交换
      • 风险 3:递归调用导致栈溢出
    • 四、方法交换的应用
      • 封装通用交换函数
      • 数组越界防护
        • 类簇问题


【iOS】方法交换(Method-Swizzling)

在 iOS 开发中,Method-Swizzling(方法交换) 是一种基于 Objective-C 运行时的高级技术,允许在程序运行时动态修改方法的实现。

一、什么是 Method-Swizzling

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

每个 Objective-C 类维护一个方法列表(methodList,每个方法由选择器(SEL)和实现(IMP)组成。通过交换两个方法的IMP,可以在不修改原有代码的前提下,动态改变方法。

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

    • OOP和AOP都是一种编程的思想
    • OOP编程思想更加倾向于对业务模块的封装,划分出更加清晰的逻辑单元;
    • AOP面向切面进行提取封装,提取各个模块中的公共部分,提高模块的复用率,降低业务之间的耦合性
  • 每个类都维护着一个方法列表,即methodListmethodList中有不同的方法Method,每个方法中包含了方法的selIMP,方法交换就是将sel和imp原本的对应断开,并将sel和新的IMP生成对应关系

原理图示

请添加图片描述

二、相关 API

Objective-C 运行时提供了一系列操作方法列表的 API,核心函数如下:

函数名 作用描述
class_getInstanceMethod获取类的实例方法(-开头的方法)
class_getClassMethod获取类的类方法(+开头的方法)
method_getImplementation获取方法的具体实现(IMP
method_setImplementation设置方法的具体实现(IMP
method_exchangeImplementations交换两个方法的实现(核心函数)
class_addMethod向类中添加新方法
class_replaceMethod替换类中已有方法的实现
  • IMP 是指向函数的指针,形如 id (*IMP)(id, SEL, ...),包含接收者(self)和选择器(_cmd)参数。

  • method_exchangeImplementations 会直接交换两个方法的IMP,适用于当前类已实现的方法交换。

三、方法交换的风险

风险 1:多次交换导致逻辑混乱

问题load 方法可能被多次调用(如分类继承链),导致方法交换重复执行,SELIMP 指向错乱。解决方案:使用 dispatch_once 确保交换逻辑只执行一次。

+ (void)load {  static dispatch_once_t onceToken;  dispatch_once(&onceToken, ^{  [self swizzleMethod];  });  
}  

风险 2:跨类交换引发崩溃

在下面这段代码:

//父类
#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface CJLPerson : NSObject
- (void)sayBye; // 原始方法
@endNS_ASSUME_NONNULL_END//父类实现
#import "CJLPerson.h"
@implementation CJLPerson
- (void)sayBye {NSLog(@"CJLPerson sayBye");
}
@end//子类定义
#import "CJLPerson.h"NS_ASSUME_NONNULL_BEGIN@interface CJLTeacher : CJLPerson
- (void)sayBye; 
@endNS_ASSUME_NONNULL_END//子类实现
#import "CJLTeacher.h"
#import <objc/runtime.h>@implementation CJLTeacher// 方法交换逻辑
+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{NSLog(@"[Load] Start method swizzling");// 交换当前类(CJLTeacher)的 sayBye 和 sayNO[self GC_MethodSwizzlingWithClass:self originalSEL:@selector(sayBye) swizzledSEL:@selector(sayNO)];});
}// 方法交换
+ (void)GC_MethodSwizzlingWithClass:(Class)cls originalSEL:(SEL)oriSEL swizzledSEL:(SEL)swiSEL {if (!cls) return;// 获取原始方法和交换方法Method originalMethod = class_getInstanceMethod(cls, oriSEL);Method swizzledMethod = class_getInstanceMethod(cls, swiSEL);method_exchangeImplementations(originalMethod, swizzledMethod);NSLog(@"[Swizzling] Success: %@ <-> %@", NSStringFromSelector(oriSEL), NSStringFromSelector(swiSEL));
}// 交换后的方法
- (void)sayNO {NSLog(@"[Before] Call original method via %@", NSStringFromSelector(@selector(sayBye)));[self sayNO]; NSLog(@"[After] Swizzled method: %s", __func__);
}@end

请添加图片描述

这里的报错也就是发现我们在这个CJLPerson类中没有找到对应的方法,因为我相当于把子类的方法交换到了父类中,父类的方法列表中找不到子类的方法,但是子类可以找到对应的方法,所以问题就是子类不可以和父类交换方法,会导致父类的方法出现问题.

如果要进行交换可以采用下面的方式

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

+ (void)GC_MethodSwizzlingWithClass:(Class)clsoriginalSEL:(SEL)oriSELswizzledSEL:(SEL)swiSEL {if (!cls) return;// 获取原始方法和交换方法Method originalMethod = class_getInstanceMethod(cls, oriSEL);Method swizzledMethod = class_getInstanceMethod(cls, swiSEL);//尝试向类中添加原始方法(处理方法未实现的情况)BOOL didAddMethod = class_addMethod(cls,oriSEL,method_getImplementation(swizzledMethod),method_getTypeEncoding(swizzledMethod));if (didAddMethod) {// 添加成功:说明原始方法未实现,将交换方法替换为原始实现class_replaceMethod(cls,swiSEL,method_getImplementation(originalMethod),method_getTypeEncoding(originalMethod));} else {// 直接交换两个方法的实现method_exchangeImplementations(originalMethod, swizzledMethod);}NSLog(@"[Swizzling] Success: %@ <-> %@", NSStringFromSelector(oriSEL), NSStringFromSelector(swiSEL));
}

即可正常实现

请添加图片描述

  • 要在当前类的方法中进行交换

    被交换的方法必须是当前类的方法,不能是父类的方法,直接把父类的实现拷贝过来不会起作用。父类的方法必须在调用的时候使用,而不是方法交换时使用。方法交换只能作用于当前类的方法,不能影响父类的方法。

风险 3:递归调用导致栈溢出

如果两个方法都没有实现会进入无限递归也就是无限循环,导致我们的一个栈溢出:

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

优化:

  • 交换后始终通过原方法名调用原始实现,避免直接使用新方法名。

  • 使用 method_getImplementation 获取原始 IMP 并缓存。

四、方法交换的应用

封装通用交换函数

+ (void)safeMethodSwizzlingForClass:(Class)cls  originalSEL:(SEL)oriSEL  swizzledSEL:(SEL)swiSEL {  if (!cls) return;  // 获取原始方法与交换方法  Method originalMethod = class_getInstanceMethod(cls, oriSEL);  Method swizzledMethod = class_getInstanceMethod(cls, swiSEL);  // 尝试向类中添加原始方法(处理父类方法未实现的情况)  BOOL didAddMethod = class_addMethod(cls,  oriSEL,  method_getImplementation(swizzledMethod),  method_getTypeEncoding(swizzledMethod));  if (didAddMethod) {  // 添加成功:说明原始方法未实现,将交换方法的实现替换为原始方法  class_replaceMethod(cls,  swiSEL,  method_getImplementation(originalMethod),  method_getTypeEncoding(originalMethod));  } else {  // 添加失败:直接交换两个方法的实现  method_exchangeImplementations(originalMethod, swizzledMethod);  }  
}  

数组越界防护

类簇问题

NSArrayNSDictionary 等 Foundation 类是类簇(Class Cluster),一个NSArray的实现可能由多个类组成。所以如果想对NSArray进行Swizzling,必须获取到其“真身”进行Swizzling,直接对NSArray进行操作是无效的

其实际实现类(真身)如下表:

公开类名 实际实现类
NSArray__NSArrayI
NSMutableArray__NSArrayM
NSDictionary__NSDictionaryI
NSMutableDictionary__NSDictionaryM
// NSArray+CrashProtection.h  
#import <Foundation/Foundation.h>
#import <objc/runtime.h>// NSArray分类实现越界保护
@interface NSArray (CrashProtection)
@end@implementation NSArray (CrashProtection)+ (void)load {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{// 处理各种数组类型// 不可变数组[self swizzleMethodWithClass:NSClassFromString(@"__NSArrayI")];// 可变数组[self swizzleMethodWithClass:NSClassFromString(@"__NSArrayM")];// 空数组[self swizzleMethodWithClass:NSClassFromString(@"__NSArray0")];// 单元素数组[self swizzleMethodWithClass:NSClassFromString(@"__NSSingleObjectArrayI")];// 常量数组 (这是上面报错的类型)[self swizzleMethodWithClass:NSClassFromString(@"NSConstantArray")];// 另一种常见的数组类型[self swizzleMethodWithClass:NSClassFromString(@"__NSPlaceholderArray")];// 直接处理NSArray类本身和子类[self swizzleArrayClass:[NSArray class]];[self swizzleArrayClass:[NSMutableArray class]];});
}// 处理NSArray类及其子类
+ (void)swizzleArrayClass:(Class)cls {// 交换objectAtIndex:方法[self swizzleMethod:cls originalSel:@selector(objectAtIndex:) swizzledSel:@selector(safe_objectAtIndex:)];// 交换objectAtIndexedSubscript:方法[self swizzleMethod:cls originalSel:@selector(objectAtIndexedSubscript:) swizzledSel:@selector(safe_objectAtIndexedSubscript:)];
}// 处理具体的类
+ (void)swizzleMethodWithClass:(Class)cls {if (!cls) return;[self swizzleArrayClass:cls];
}// 方法交换的核心实现
+ (void)swizzleMethod:(Class)cls originalSel:(SEL)originalSel swizzledSel:(SEL)swizzledSel {Method originalMethod = class_getInstanceMethod(cls, originalSel);Method swizzledMethod = class_getInstanceMethod([self class], swizzledSel);if (!originalMethod || !swizzledMethod) return;// 先尝试给原类添加方法实现BOOL didAddMethod = class_addMethod(cls, swizzledSel, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));if (didAddMethod) {// 添加成功后,用原始方法替换添加的方法Method newMethod = class_getInstanceMethod(cls, swizzledSel);method_exchangeImplementations(originalMethod, newMethod);} else {// 添加失败,说明已经存在这个方法,直接交换method_exchangeImplementations(originalMethod, swizzledMethod);}
}// 安全的objectAtIndex:方法实现
- (id)safe_objectAtIndex:(NSUInteger)index {if (index >= self.count) {NSLog(@"[数组越界警告] 尝试访问的索引 %lu 超出了数组范围 (数组长度: %lu, 类型: %@)", (unsigned long)index, (unsigned long)self.count, NSStringFromClass([self class]));return nil;}return [self safe_objectAtIndex:index];
}// 安全的objectAtIndexedSubscript:方法实现(处理数组下标访问)
- (id)safe_objectAtIndexedSubscript:(NSUInteger)index {if (index >= self.count) {NSLog(@"[数组越界警告] 尝试通过下标 %lu 访问超出了数组范围 (数组长度: %lu, 类型: %@)", (unsigned long)index, (unsigned long)self.count, NSStringFromClass([self class]));return nil;}return [self safe_objectAtIndexedSubscript:index];
}@endint main(int argc, const char * argv[]) {@autoreleasepool {NSArray *array = @[@"一", @"二", @"三"];NSLog(@"数组类型: %@", NSStringFromClass([array class]));// 正常访问NSLog(@"正常访问: %@", array[1]);// 越界访问 - 使用下标方式NSLog(@"越界访问[5]: %@", array[5]);// 越界访问 - 使用objectAtIndex方式NSLog(@"越界访问(objectAtIndex:10): %@", [array objectAtIndex:10]);// 测试空数组NSArray *emptyArray = @[];NSLog(@"空数组类型: %@", NSStringFromClass([emptyArray class]));NSLog(@"空数组越界访问[0]: %@", emptyArray[0]);// 测试可变数组NSMutableArray *mutableArray = [NSMutableArray arrayWithObjects:@"A", @"B", @"C", nil];NSLog(@"可变数组类型: %@", NSStringFromClass([mutableArray class]));NSLog(@"可变数组越界访问[5]: %@", mutableArray[5]);}return 0;
}

结果如下图:
请添加图片描述

五、注意事项

  1. **仅在 **load方法中执行交换:load 方法在类加载时自动调用,早于其他方法执行,确保交换逻辑优先生效。

  2. **避免依赖 **_cmd 参数:交换后 SELIMP 的映射关系改变,直接使用 _cmd 可能导致选择器匹配错误。

  3. 单元测试验证:复杂交换逻辑需通过单元测试覆盖边界情况(如递归调用、父类方法覆盖)。

http://www.dtcms.com/a/295822.html

相关文章:

  • 告别束缚:这款“隐形心电监测仪”让心脏健康管理更自由
  • JavaSE:开发环境的搭建(Eclipse)
  • 企业级数据分析创新实战:基于表格交互与智能分析的双引擎架构
  • 从0到1学习c++ 命名空间
  • 《 java 随想录》| 数组
  • MySQL的命令行客户端
  • 探索双链表:C语言中的链式结构魔法
  • 光谱仪杂散光性能分析
  • 大疆无人机炸机后视频损坏的完美修复案例解析
  • uni-file-picker vue3二次封装(本地上传 + v-model)
  • Mysql命令show processlist
  • Linux基础服务(autofs和Samba)
  • 论文阅读:《针对多目标优化和应用的 NSGA-II 综述》一些关于优化算法的简介
  • OpenCV —— color_matrix_numpy_mat_reshape
  • 新mac电脑软件安装指南(前端开发用)
  • 解决http的web服务中与https服务交互的问题
  • 平时遇到的错误码及场景?404?400?502?都是什么场景下什么含义,该怎么做 ?
  • AI实践:Pydantic
  • 大模型Prompt优化工程
  • pdf文件的属性值怎么修改?修改PDF内部的属性创建时间和修改时间
  • Lua(垃圾回收)
  • omofun官网网站入口,动漫在线看|官方下载
  • AI服务器给一体成型电感带来多大的市场空间
  • 网络编程——聊天程序实现
  • FreeSWITCH 简单图形化界面45 - 收集打包的一些TTS
  • 复矩阵与共轭转置矩阵乘积及其平方根矩阵
  • 【建模与仿真】融合共现网络特征与知识增强语义梯度提升电子邮件分类
  • HttpServletRequest深度解析:Java Web开发的核心组件
  • LLM中的位置嵌入矩阵(Position Embedding Matrix)是什么
  • [语言模型训练]基于 PyTorch 的双向 LSTM 文本分类器实现:基于旅店的评论分类语言模型