【iOS】源码学习-方法交换
前言
方法交换就是传说中的"iOS黑魔法",其主要作用是在运行时将一个方法的实现替换成另一个方法的实现。
方法交换定义
在OC中就是利用方法交换实现AOP。每个类都维护一个方法列表methodList,方法列表中有不同的方法Method,每个方法中包含了方法的SEL和IMP。方法交换就是将SEL和IMP原本的对应断开,并将新SEL和新IMP生成对应关系。
OOP和AOP都是一种编程的思想:
- OOP:面向对象编程。编程思想更加倾向于对业务模块的封装,划分出更加清晰的逻辑单元。
- AOP:面向切面编程。是一种通过分离横切关注点来增强代码模块化的编程范式。它通过动态注入逻辑的方式,将与核心业务无关的通用功能(如日志、事务、权限控制等)从代码中解耦,从而提升可维护性和复用性。

方法交换API
通过SEL获取方法Method:
- class_getInstanceMethod():获取实例方法
- class_getClassMethod():获取类方法
- method_getImplementation:获取一个方法的实现
- method_setImplementation:设置一个方法的实现
- method_getTypeEncoding:获取方法实现的编码类型:method_getTypeEncoding
- class_addMethod:添加方法实现
- class_replaceMethod:用一个方法的实现替换另一个方法的实现,即aIMP指向bIMP,但是bIMP不一定指向aIMP
- method_exchangeImplementations:交换两个方法的实现,即aIMP指向bIMP,但是bIMP也指向aIMP
方法交换坑点
使用一次性问题
method-swizzling必须在+load方法中执行(这保证了在任何对象调用该方法之前,交换已完成),而load反复调用多次,这样会导致方法的重复交换,使方法SEL的指向又恢复为原来的IMP。可以通过在dispatch_once中实现单例使方法交换只执行一次。
objc
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LGRuntimeTool lg_bestMethodSwizzlingWithClass:self oriSEL:@selector(helloword) swizzledSEL:@selector(lg_studentInstanceMethod)];
});
}
使用dispatch_once是为了确保交换逻辑的线程安全和幂等性(不因重复执行而出问题,即同一个操作无论执行多少次最终效果完全一样),即使在多线程环境下也只执行一次。
子类没有实现,父类实现
在子类分类中交换一个子类未实现、父类实现的实例方法,使用method_exchangeImplementations会导致父类实例调用时崩溃。
父类LYDPerson:
objc
@interface LYDPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation LYDPerson
- (void)personInstanceMethod {
NSLog(@"person对象方法:%s", __func__);
}
子类LYDStudent:什么都没有实现

分类LYD:
objc
@interface LYDStudent (LYD)
- (void)lyd_studentInstanceMethod;
@end
@implementation LYDStudent (LYD)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LYDRuntimeTool lyd_methodSwizzlingWithClass:self originSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lyd_studentInstanceMethod)];
});
}
- (void)lyd_studentInstanceMethod {
[self lyd_studentInstanceMethod];
NSLog(@"LYDStudent分类lyd_studentInstanceMethod");
}
@end
方法交换:
objc
+ (void)lyd_methodSwizzlingWithClass:(Class)cls originSEL:(SEL)originSEL swizzledSEL:(SEL)swizzledSEL {
if (!cls) {
return;
}
// cls=LYDStudent originSEL=personInstanceMethod
// 子类没有实现这个方法,class_getInstanceMethod沿着继承链向上找,最终拿到父类LYDPerson里的这个方法,而不是子类自己的
// 因此LYDStudent的personInstanceMethod和LYDStudent分类的lyd_studentInstanceMethod方法交换实际上是LYDPerson的personInstanceMethod和LYDStudent分类的lyd_studentInstanceMethod方法交换
Method originMethod = class_getInstanceMethod(cls, originSEL);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSEL);
method_exchangeImplementations(originMethod, swizzledMethod);
}
运行结果:

从日志我们可以看出崩溃的原因是子类LYDStudent本身没有personInstanceMethod方法,于是会向上查找父类LYDPerson的Method。然后执行method_exchangeImplementations会把LYDPerson里的personInstanceMethod IMP和LYDStudent分类里的lyd_studentInstanceMethod IMP互换。这样父类LYDPerson的personInstanceMethod指向子类分类的实现,而父类本身没有lyd_studentInstanceMethod。因此p调用personInstanceMethod时,p走到子类分类的方法,父类找不到该方法,找不到IMP,消息未识别,直接崩溃。
解决办法:优先使用class_addMethod尝试给当前子类添加原方法。

子类没有实现,父类也没有实现
目标方法在整个继承链中完全无实现,class_getInstanceMethod返回nil,method_exchangeImplementations遇到nil参数,直接放弃执行,即没有交换。而lg_studentInstanceMethod依旧指向自己原本的代码,方法实现里又调用自己,进而陷入死循环。

解决办法:增加前置判空。
先判断原方法是否为空,为空则手动给原方法补实现,并把交换方法改成空Block,彻底切断自调用。

方法依赖_cmd
_cmd:关键字,类型是SEL,每个OC方法天生自带的隐藏参数。
OC实例方法底层本质是C函数:
void 方法底层函数(id self, SEL _cmd, 其他参数...)
- self:当前调用的对象
- _cmd:本次调用使用的方法名SEL
objc
@implementation LYDPerson
- (void)run {
SEL currentSel = _cmd;
NSString *selName = NSStringFromSelector(currentSel);
if ([selName isEqualToString:@"run"]) {
NSLog(@"调用run");
} else {
NSLog(@"没调用");
}
}
- (void)lyd_run {
NSLog(@"原生lyd_run方法");
}
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [self class];
Method runMethod = class_getInstanceMethod(cls, @selector(run));
Method lydRunMethod = class_getInstanceMethod(cls, @selector(lyd_run));
method_exchangeImplementations(runMethod, lydRunMethod);
});
}
@end

交换后,调用方法run,run指向了lyd_run的代码,直接打印原生lyd_run方法。调用方法lyd_run,指向了run的代码,进入run函数体,函数内_cmd为当前调用名lyd_run,因此走else分支,输出没调用。
方法逻辑依赖_cmd做判断,一旦方法交换,判断实效,原有业务逻辑被破坏。
类方法和实例方法的交换方法原理类似,唯一的区别是类方法存在元类中。在调用class_addMethod和class_replaceMethod方法添加和替换时,需要传入的类是元类。可以通过object_getClass方法获取类的元类。
方法交换应用
方法交换最常用的应用是防止数组、字典等越界崩溃。
在iOS中NSNumber、NSArray、NSDictionary等这些类都是类族,一个NSArray的实现可能由多个类组成。所以如果想对NSArray进行方法交换,必须取到真身进行,直接对NSArray进行操作是无效的。
objc
#import "NSArray+SafeCrash.h"
#import <objc/runtime.h>
@implementation NSArray (SafeCrash)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"load方法");
Method firstMethod = class_getInstanceMethod(objc_getClass("NSConstantArray"), @selector(objectAtIndex:));
Method secondMethod = class_getInstanceMethod(objc_getClass("NSConstantArray"), @selector(new_objectAtIndex:));
method_exchangeImplementations(firstMethod, secondMethod);
});
}
- (id)new_objectAtIndex:(NSUInteger)index {
if (index >= self.count) {
NSLog(@"数组越界:下标%lu,数组总数%lu", (unsigned long)index, (unsigned long)self.count);
return nil;
} else {
// 交换后,调用new_objectAtIndex等价于调用原生objectAtIndex
return [self new_objectAtIndex:index];
}
}
@end

这样无论有没有越界访问,都不会报错,正常输出。
总结
方法交换时机由dyld决定,dyld负责把程序和库加载进内存并触发+load,Runtime负责真正交换方法IMP。因此,方法交换必须写在+load里,由dyld触发,保证在第一次使用类之前完成交换,并且使用dispatch_once保证方法交换只执行一次,避免重复交换逻辑错乱。