黑魔法
【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; // 原始方法
@end
NS_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;
@end
NS_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];
}
@end
int 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
可能导致选择器匹配错误。单元测试验证:复杂交换逻辑需通过单元测试覆盖边界情况(如递归调用、父类方法覆盖)。