文章目录
- 前言
- [Method Swizzling 是什么](#Method Swizzling 是什么)
- 核心源码分析
-
- [method_exchangeImplementations 的作用](#method_exchangeImplementations 的作用)
- [class_replaceMethod 的区别](#class_replaceMethod 的区别)
- [class_addMethod 的用途](#class_addMethod 的用途)
- 一个最简单的例子
- 三大坑点
- [额外注意:`_cmd` 依赖问题](#额外注意:
_cmd依赖问题) - 类方法怎么交换
- 这是怎么跟消息发送流程串起来的
- [为什么写在 +load 里](#为什么写在 +load 里)
- 实际应用:防数组越界
- 可以直接用的安全封装
- 常见问题
-
-
- [1. Method Swizzling 的原理是什么?](#1. Method Swizzling 的原理是什么?)
- [2. 为什么写在 +load 里?为什么用 dispatch_once?](#2. 为什么写在 +load 里?为什么用 dispatch_once?)
- [3. 子类没实现、父类实现了,直接交换会怎样?](#3. 子类没实现、父类实现了,直接交换会怎样?)
- [4. class_replaceMethod 和 method_exchangeImplementations 的区别?](#4. class_replaceMethod 和 method_exchangeImplementations 的区别?)
- [5. 给 NSArray 做 Swizzling 要注意什么?](#5. 给 NSArray 做 Swizzling 要注意什么?)
- [6. 交换后调用自定义方法为什么不递归?](#6. 交换后调用自定义方法为什么不递归?)
- [7. Swizzling 有什么风险?](#7. Swizzling 有什么风险?)
-
- 总结
前言
上一篇 消息流程探索我们把 objc_msgSend 从快速查找 cache -> 慢速查找继承链 -> 动态方法决议 -> 消息转发都过了一遍。分类那篇也讲了分类的加载时机。
这篇我们聊这两个东西的交汇点------Method Swizzling,也就是传说中的 iOS 黑魔法。
Method Swizzling 是什么
Method Swizzling 就是在运行时把两个方法的实现互换。
每个类都有一张方法列表(methodList),里面每个 Method 包含 SEL(方法名)和 IMP(函数指针)。方法交换,就是把 SEL 和 IMP 的对应关系改掉
如图,交换前后的sel和IMP的对应关系:

这就相当于给方法搞了个"别名"------你想在 viewWillAppear: 里加点日志,不用去改每个控制器的代码,直接交换就行。
这种思想叫 AOP(面向切面编程),跟 OOP 的区别简单来说:
- OOP:把业务逻辑封装成各种类(UserManager、OrderManager)
- AOP:把日志、埋点、权限这些跟业务无关的东西抽出来,统一织入
在 OC 里做 AOP,主要手段就是 Runtime 的方法交换和消息转发。实际场景比如:
- 日志/埋点:统一记录页面访问,不用每个 VC 写一遍 NSLog
- 防崩溃 :Hook 掉 NSArray 的
objectAtIndex:,越界时返回 nil 而不是 Crash - 性能监控:统计方法耗时
- 权限检查:调用前统一校验
前置知识:SEL、IMP、Method
SEL(方法选择器)
c
typedef struct objc_selector *SEL;
SEL 就是方法名的唯一标识。编译时生成,Runtime 有个全局哈希表来存,保证同样的方法名 SEL 唯一。这也是 OC 不能像 C++ 那样重载的原因------同名方法 SEL 一样,没法区分。
可以理解为"书名"
IMP(方法实现指针)
c
typedef void (*IMP)(void /* id, SEL, ... */ );
IMP 就是个函数指针,指向方法实现代码在内存里的地址。objc_msgSend 最终就是跳转到这个地址去执行。
理解为"书翻到的页码"
Method(方法描述)
c
typedef struct objc_method *Method;
struct objc_method {
SEL _Nonnull method_name;
char * _Nullable method_types;
IMP _Nonnull method_imp;
};
Method 就是把 SEL 和 IMP 打包在一起的结构体,相当于"书的某一章节的完整内容"。
方法交换的本质就是互换 Method 结构体里的 method_imp 指针------SEL 本身不变,IMP 换了个指向
核心源码分析
method_exchangeImplementations 的作用
下面这段是 源码里 method_exchangeImplementations 的实现:
cpp
void method_exchangeImplementations(Method m1Signed, Method m2Signed)
{
// 1. nil 保护
if (!m1Signed || !m2Signed) return;
method_t *m1 = _method_auth(m1Signed);
method_t *m2 = _method_auth(m2Signed);
// 2. 加锁
mutex_locker_t lock(runtimeLock);
// 3. 取出两个 Method 的 IMP 和 SEL
IMP imp1 = m1->imp(false);
IMP imp2 = m2->imp(false);
SEL sel1 = m1->name();
SEL sel2 = m2->name();
// 4. 核心:交换 IMP
m1->setImp(imp2);
m2->setImp(imp1);
// 5. 刷新所有类的缓存
flushCaches(nil, __func__, [sel1, sel2, imp1, imp2](Class c){
return c->cache.shouldFlush(sel1, imp1) || c->cache.shouldFlush(sel2, imp2);
});
// 6. 调整 retain/release/alloc 等特殊标记
adjustCustomFlagsForMethodChange(nil, m1);
adjustCustomFlagsForMethodChange(nil, m2);
}
核心就四件事:
| 步骤 | 干啥的 |
|---|---|
| nil 保护 | 传空直接 return,别崩 |
| 加锁 | runtimeLock 保证线程安全 |
| 交换 IMP | 就是 m1->setImp(imp2) + m2->setImp(imp1),指针互换 |
| flushCaches | 交换完必须刷缓存,不然 objc_msgSend 快速查找可能命中旧的 IMP |
文档里说这个函数等价于以下操作的原子版本:
objc
IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);
class_replaceMethod 的区别
cpp
IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
{
if (!cls) return nil;
mutex_locker_t lock(runtimeLock);
return addMethod(cls, name, imp, types ?: "", YES);
}
这个跟 method_exchangeImplementations 最大的区别是------
| API | 方向 | 效果 |
|---|---|---|
class_replaceMethod |
单向 | A 的 IMP 换成 B 的,但 B 的 IMP 不变,原来的 IMP 丢了 |
method_exchangeImplementations |
双向 | A 和 B 的 IMP 互换,通过别名还能调到原来的实现 |
在做 Swizzling 的时候我们想要的是双向交换------这样在自定义方法里 [self ypp_xxx] 才能调到原始实现。所以最常用的还是 method_exchangeImplementations。
class_addMethod 的用途
objc
OBJC_EXPORT BOOL
class_addMethod(Class cls, SEL name, IMP imp, const char *types);
这个返回值很有意思:
- 返回 YES:说明加成功了 -> 当前类没有这个方法(从父类继承来的),现在给它新加了一个
- 返回 NO:说明加失败了 -> 当前类自己已经有这个方法了
这个特性就是后面解决"子类没实现父类实现了"这个坑的关键
一个最简单的例子
objc
@implementation UIViewController (Swizzling)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod = class_getInstanceMethod(self, @selector(viewWillAppear:));
Method swizzledMethod = class_getInstanceMethod(self, @selector(ypp_viewWillAppear:));
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
- (void)ypp_viewWillAppear:(BOOL)animated {
// 这里实际上调的是原始的 viewWillAppear:(因为 IMP 已经交换了)
[self ypp_viewWillAppear:animated];
NSLog(@" %@ 页面出现了", NSStringFromClass([self class]));
}
@end
调用链路是这样的:
[someVC viewWillAppear:] 调用
→ objc_msgSend 找 viewWillAppear: 的 IMP
→ 找到的 IMP 是 ypp_viewWillAppear:(已交换)
→ 执行 ypp_viewWillAppear: 里的代码
→ 里面调 [self ypp_viewWillAppear:]
→ objc_msgSend 找 ypp_viewWillAppear: 的 IMP
→ 找到的 IMP 是原始的 viewWillAppear:(已交换)
→ 执行原始实现
所以不会递归------交换是双向的,新的方法名指向旧的实现,旧的实现又能通过新代码包装
三大坑点
下面有三个比较容易出现的坑
一:重复交换
假设某个方法被交换了两次:
第一次交换:viewWillAppear: → ypp_xxx //没问题
第二次交换:viewWillAppear: → 原始实现 //又变回到原始实现
为什么会出现二次交换?最常见的原因是没有用 dispatch_once 保护
objc
// 正确做法
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 交换代码
});
}
二:子类没实现,父类实现了
这是最经典的崩溃场景
objc
@interface LGPerson : NSObject
- (void)personInstanceMethod;
@end
@interface LGStudent : LGPerson // 没有重写 personInstanceMethod
@end
// LGStudent 的分类
+ (void)load {
Method oriMethod = class_getInstanceMethod([LGStudent class], @selector(personInstanceMethod));
Method swiMethod = class_getInstanceMethod([LGStudent class], @selector(lg_studentInstanceMethod));
method_exchangeImplementations(oriMethod, swiMethod);
}
乍一看没问题,但运行会崩
原因:class_getInstanceMethod 会沿继承链查找。LGStudent 自己没实现,所以返回的是 LGPerson 的 Method 结构体 。交换后,父类 LGPerson 的 personInstanceMethod 的 IMP 指向了 LGStudent 分类里的方法。当父类对象调用这个方法时,里面如果调了 [self lg_studentInstanceMethod],父类去哪找这个方法?找不到,直接 unrecognized selector 崩溃。
解决方案:用 class_addMethod 判断方法归属。
objc
+ (void)safeSwizzleWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL {
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
if (!oriMethod || !swiMethod) return;
// 尝试给当前类添加 oriSEL,IMP 指向 swiMethod
BOOL success = class_addMethod(cls, oriSEL,
method_getImplementation(swiMethod),
method_getTypeEncoding(oriMethod));
if (success) {
// 添加成功 → 说明 cls 没有这个方法(来自父类)
// 把 swizzledSEL 指向原 IMP
class_replaceMethod(cls, swizzledSEL,
method_getImplementation(oriMethod),
method_getTypeEncoding(oriMethod));
} else {
// 添加失败 → 说明 cls 自己实现了,直接交换
method_exchangeImplementations(oriMethod, swiMethod);
}
}
这里的逻辑其实挺巧的:
| 情况 | class_addMethod 返回值 |
说明 |
|---|---|---|
| 类自己实现了该方法 | NO | 直接 method_exchangeImplementations 安全 |
| 类没有该方法(从父类继承) | YES | 用 class_replaceMethod 处理新 SEL 的指向 |
三:两个方法都没实现
父类只有声明没有实现,子类也没有:
objc
@interface LGPerson : NSObject
- (void)personInstanceMethod; // 只声明,没写实现
@end
这时候 oriMethod 就是 nil。method_exchangeImplementations 源码里虽然有 nil 保护不会崩,但交换也没生效。调用这个方法时走完整个消息转发流程,没人处理,最终 unrecognized selector 崩溃。
还有一种更隐蔽的情况:oriMethod 拿到了,但 IMP 指向的是 _objc_msgForward_impcache(消息转发标记),交换后调用新方法会触发转发,万一转发也没处理,就递归死循环栈溢出。
解决方案:加 nil 兜底
objc
if (!oriMethod) {
// 给 oriSEL 加一个空实现
class_addMethod(cls, oriSEL,
method_getImplementation(swiMethod),
method_getTypeEncoding(swiMethod));
// 把 swiMethod 的 IMP 设为空 block
method_setImplementation(swiMethod,
imp_implementationWithBlock(^(id self, SEL _cmd) {
NSLog(@"[Warning] %s 没有实现", sel_getName(oriSEL));
}));
return;
}
额外注意:_cmd 依赖问题
还有一个容易被忽略的点:如果我们的方法实现依赖 _cmd(那个隐式的 SEL 参数),交换后可能会出问题
_cmd 是每个 OC 方法都有的隐式参数,代表当前方法的 SEL。交换之后,别人调用原始方法名时,实际执行的是你自定义方法的代码,但此时 _cmd 的值仍然是原始方法的 SEL,而不是你自定义方法的 SEL
objc
- (void)originalMethod {
// 交换前:_cmd == @selector(originalMethod)
// 交换后:如果有人调 originalMethod,实际执行下面的代码,
// 但 _cmd 仍然是 @selector(originalMethod)
}
- (void)ypp_swizzledMethod {
// 交换前:_cmd == @selector(ypp_swizzledMethod)
// 交换后:调 ypp_swizzledMethod 时,_cmd 是 @selector(originalMethod)
}
如果你的自定义方法内部用 _cmd 做判断(比如根据方法名走不同逻辑),这种行为就不符合预期了。不过大多数日常埋点、日志场景不依赖 _cmd,所以这个坑遇到的不多
类方法怎么交换
实例方法存在类的方法列表里,类方法存在元类(metaclass) 的方法列表里。所以交换类方法时需要操作元类。
objc
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class metaClass = object_getClass(self); // 获取元类
Method oriMethod = class_getClassMethod(self, @selector(originalClassMethod));
Method swiMethod = class_getClassMethod(self, @selector(swizzledClassMethod));
// class_addMethod/class_replaceMethod 要传元类
BOOL success = class_addMethod(metaClass, @selector(originalClassMethod), ...);
if (success) {
class_replaceMethod(metaClass, @selector(swizzledClassMethod), ...);
} else {
method_exchangeImplementations(oriMethod, swiMethod);
}
});
}
object_getClass(self)和[self class]的区别:
[self class]在类方法里返回类对象本身object_getClass(self)返回self的 isa 指向------在类方法里就是元类因为要操作元类的方法列表,所以必须用
object_getClass。
这是怎么跟消息发送流程串起来的
我们在 消息流程探索讲过 objc_msgSend 的完整路径:
objc_msgSend(receiver, sel)
├─ 快速查找:cache_getImp
├─ 慢速查找:lookUpImpOrForward
│ ├─ 本类 method list
│ ├─ 父类 cache → 父类 method list
│ └─ ... 沿继承链向上
├─ 动态方法决议
└─ 消息转发
Swizzling 到底改了什么? 它改的是 method_list 里 Method 的 imp 指针。
交换之后,方法列表里 SEL 对应的 IMP 已经变了。下次 objc_msgSend 走慢速查找时,拿到的就是新 IMP。
但是------如果旧的 IMP 已经被缓存到 cache 里了怎么办? 这就是 flushCaches 的作用。method_exchangeImplementations 源码里调了 flushCaches(nil, ...) 来清空所有类的缓存,迫使下次走慢速查找,拿到最新的 IMP。
一条理解:Swizzling 在"源头"(method_list)改了 IMP,然后通过刷 cache 让"缓存"(cache_t)也更新,最终
objc_msgSend无论怎么查都是新的 IMP。
为什么写在 +load 里
结合 分类的加载时机来看。
分类的方法是在 attachLists 阶段合并到本类的。+load 方法执行的时候,所有分类数据已经合并完毕了,所以这时候做交换是安全的。
整个时间线:
App 启动 → dyld 加载 Mach-O
→ _objc_init(注册回调)
→ map_images / _read_images(读类、处理分类元数据)
→ realizeClass + methodizeClass(分类合并到本类)
→ load_images(执行 +load 方法) ← 这时候分类已经合并完了
那为什么是 +load 而不是 +initialize
| +load | +initialize | |
|---|---|---|
| 调用时机 | App 启动时,main 之前 | 第一次给类发消息时 |
| 线程安全 | 单线程串行 | 多线程环境 |
| 适不适合 Swizzling | 最早最确定 | 时机不确定 |
另外注意:不要在 +load 里调 [super load]
Runtime 已经在 load_images 里调过父类的 +load 了,你再手动调一次,父类的交换代码就执行了两遍------可能造成重复交换
实际应用:防数组越界
这是 Swizzling 最经典的实战场景。但有个坑:NSArray 是个类簇(Class Cluster)
什么叫类簇?就是 NSArray 只是个抽象父类,真正的实现是它的私有子类:
objc
NSArray *a = @[@"a"]; // 实际是 __NSArrayI
NSArray *b = @[]; // 实际是 __NSArray0
NSMutableArray *m = @[].mutableCopy; // 实际是 __NSArrayM
对 NSArray 做 Swizzling 没有用,必须找到真身类:
objc
@implementation NSArray (SafeAccess)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class arrayCls = objc_getClass("__NSArrayI");
Method from = class_getInstanceMethod(arrayCls, @selector(objectAtIndex:));
Method to = class_getInstanceMethod(arrayCls, @selector(ypp_safe_objectAtIndex:));
method_exchangeImplementations(from, to);
});
}
- (id)ypp_safe_objectAtIndex:(NSUInteger)index {
if (index >= self.count) {
NSLog(@" 越界: %lu >= %lu", (unsigned long)index, (unsigned long)self.count);
return nil;
}
return [self ypp_safe_objectAtIndex:index]; // 调的是原始的 objectAtIndex:
}
@end
字面量 arr[0] 走的是 objectAtIndexedSubscript:,所以也需要单独 Hook。
常见类簇真身表:
| 公开类 | 真身类 |
|---|---|
NSArray |
__NSArrayI |
NSArray(空) |
__NSArray0 |
NSArray(单元素) |
__NSSingleObjectArrayI |
NSMutableArray |
__NSArrayM |
NSDictionary |
__NSDictionaryI |
NSMutableDictionary |
__NSDictionaryM |
可以直接用的安全封装
把前面三个坑的解决方案合在一起:
objc
+ (void)ypp_safeSwizzleWithClass:(Class)cls
oriSEL:(SEL)oriSEL
swizzledSEL:(SEL)swizzledSEL {
if (!cls || !oriSEL || !swizzledSEL) return;
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
// 情况1:原方法不存在 → 添加空实现兜底
if (!oriMethod) {
class_addMethod(cls, oriSEL,
method_getImplementation(swiMethod),
method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod,
imp_implementationWithBlock(^(id self, SEL _cmd) {
NSLog(@"[空实现] %s", sel_getName(oriSEL));
}));
return;
}
// 情况2:新方法不存在 → 同样兜底
if (!swiMethod) {
class_addMethod(cls, swizzledSEL,
method_getImplementation(oriMethod),
method_getTypeEncoding(oriMethod));
return;
}
// 情况3:判断方法归属
BOOL success = class_addMethod(cls, oriSEL,
method_getImplementation(swiMethod),
method_getTypeEncoding(oriMethod));
if (success) {
class_replaceMethod(cls, swizzledSEL,
method_getImplementation(oriMethod),
method_getTypeEncoding(oriMethod));
} else {
method_exchangeImplementations(oriMethod, swiMethod);
}
}
使用:
objc
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self ypp_safeSwizzleWithClass:self
oriSEL:@selector(viewWillAppear:)
swizzledSEL:@selector(ypp_viewWillAppear:)];
});
}
常见问题
1. Method Swizzling 的原理是什么?
方法列表里每个 Method 有 SEL 和 IMP,交换就是把两个 Method 的 IMP 指针互换。调用 method_exchangeImplementations 后,内核就是 m1->setImp(imp2) + m2->setImp(imp1),然后 flushCaches 刷缓存保证下次查找拿到新 IMP。
2. 为什么写在 +load 里?为什么用 dispatch_once?
+load 是 App 启动时最早调用的,且分类数据已经合并完毕,时机最确定。dispatch_once 保证交换代码只执行一次,防止重复交换导致失效。
3. 子类没实现、父类实现了,直接交换会怎样?
会崩。class_getInstanceMethod 返回的是父类的 Method,交换后父类方法的 IMP 指向了子类分类的方法,父类对象调用时找不到子类分类的方法。用 class_addMethod 判断归属后分情况处理。
4. class_replaceMethod 和 method_exchangeImplementations 的区别?
前者是单向替换,原来的 IMP 就丢了;后者是双向交换,通过别名可以访问原实现。
5. 给 NSArray 做 Swizzling 要注意什么?
NSArray 是类簇,要对真身类(__NSArrayI)做交换,而不是 NSArray 本身。
6. 交换后调用自定义方法为什么不递归?
因为交换是双向的。自定义方法的 SEL 指向的是原始实现,所以 [self ypp_xxx] 实际上调的是原始方法,不是自己。
7. Swizzling 有什么风险?
- 没加 dispatch_once -> 重复交换
- 没做归属判断 -> 影响父类
- 方法名没加前缀 -> 跟别人的库冲突
- 类簇直接 Swizzling -> 无效
- 多个库 Hook 同一个方法 -> 互相覆盖
总结
Method Swizzling 的本质一句话:运行时交换 SEL 和 IMP 的对应关系。
回头看 method_exchangeImplementations 那几十行源码,核心其实就两件事:m1->setImp(imp2) + m2->setImp(imp1) 把指针互换,再调 flushCaches 清掉旧缓存。没有魔法。
安全使用的核心三点:
dispatch_once防重复交换class_addMethod判断归属,防父类污染- nil 兜底,防递归崩溃
搞懂了消息发送流程和分类加载时机,Swizzling 就没那么神秘了------它只是在 method_list 这一层改了改指针指向,再通过 flushCaches 让下次 objc_msgSend 拿到新的 IMP 而已