「iOS」黑魔法——方法交换
黑魔法
- 【iOS】方法交换(Method-Swizzling)
【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
是面向切面进行提取封装,提取各个模块中的公共部分,提高模块的复用率,降低业务之间的耦合性
。
-
每个类都维护着一个
方法列表
,即methodList
,methodList
中有不同的方法
即Method
,每个方法中包含了方法的sel
和IMP
,方法交换就是将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
方法可能被多次调用(如分类继承链),导致方法交换重复执行,SEL
与 IMP
指向错乱。解决方案:使用 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)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); } 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); } }
数组越界防护
类簇问题
NSArray
、NSDictionary
等 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;}
结果如下图:
五、注意事项
-
**仅在 **
load
方法中执行交换:load
方法在类加载时自动调用,早于其他方法执行,确保交换逻辑优先生效。 -
**避免依赖 **
_cmd
参数:交换后SEL
与IMP
的映射关系改变,直接使用_cmd
可能导致选择器匹配错误。 -
单元测试验证:复杂交换逻辑需通过单元测试覆盖边界情况(如递归调用、父类方法覆盖)。