「iOS」黑魔法——方法交换
黑魔法
【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)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); }
}
数组越界防护
类簇问题
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
可能导致选择器匹配错误。 -
单元测试验证:复杂交换逻辑需通过单元测试覆盖边界情况(如递归调用、父类方法覆盖)。