JRSwizzle源码逐行解读:method_exchangeImplementations与方法交换全机制解析
【免费下载链接】jrswizzleone-stop-shop for all your method swizzling needs项目地址: https://gitcode.com/gh_mirrors/jr/jrswizzle
JRSwizzle 是 Objective-C 开发中处理**方法交换(Method Swizzling)**的"一站式"开源库,核心代码仅两个文件、不到 200 行。本文将逐行解读它的源码,带你彻底搞懂method_exchangeImplementations的工作原理、继承场景下的"方法提升"技巧,以及它与class_addMethod的经典配合机制——即使你是运行时新手,也能轻松读懂。
一、方法交换解决什么问题?🔁
在 Objective-C 中,[obj doSomething]这行消息发送的本质是:在类的方法列表里根据选择器(SEL)找到对应方法的实现函数指针(IMP),然后跳转执行。
方法交换就是:在运行时把两个方法的 IMP 对调。交换之后,方法名和签名不变,但实际执行的是对方的代码。常见用途:
- 给第三方库的方法"无侵入"加日志、埋点
- A/B 测试中临时替换业务逻辑
- 监控或拦截系统框架的调用
但方法交换有一个新手最容易踩的坑:继承。如果子类的某个方法是从父类继承来的,直接对父类方法动手,会把整条继承链全部改坏。JRSwizzle 存在的最大意义,就是在任意系统版本上正确处理这个继承问题。
二、项目结构一览:核心代码就两个文件 📦
| 文件 | 作用 |
|---|---|
| JRSwizzle.h | 公开 API 声明,只有 4 个类方法 |
| JRSwizzle.m | 全部核心实现(约 166 行) |
| JRSwizzleTest/JRSwizzleTest.m | 继承/直接实现的正确性测试 |
| JRSwizzleTest/MethodSwizzle.m | 早期 Ballard 实现的对照参考 |
| JRSwizzle.podspec | CocoaPods 描述文件(iOS 4.3+ / macOS 10.6+) |
整个库不依赖任何第三方,只依赖Foundation和 ObjC 运行时头文件。
三、四种交换 API 速览 ⚡
JRSwizzle.h 中通过NSObject的类别暴露了 4 个 API:
| API | 用途 |
|---|---|
jr_swizzleMethod:withMethod:error: | 交换两个实例方法(最常用) |
jr_swizzleClassMethod:withClassMethod:error: | 交换两个类方法 |
jr_swizzleMethod:withBlock:error: | 用Block 注入替换实例方法 |
jr_swizzleClassMethod:withBlock:error: | 用 Block 注入替换类方法 |
最基础的使用方式,一行搞定:
[SomeClass jr_swizzleMethod:@selector(foo) withMethod:@selector(my_foo) error:&error];所有 API 都返回NSError**出参,找不到方法时会写入高质量诊断信息,这是它在"鲁棒性"上优于早期实现的体现。
四、核心源码逐行解读:jr_swizzleMethod 三步走 💡
下面是对 JRSwizzle.m 中jr_swizzleMethod:withMethod:error:的逐步拆解(现代运行时路径,即OBJC_API_VERSION >= 2)。
第 0 步:兼容性预处理
文件开头有两处为"兼容老系统"服务的定义:
- JRSwizzle.m:iOS 与 macOS 需要的运行时头文件不同(
objc/runtime.hvsobjc/objc-class.h); - JRSwizzle.m:老运行时没有
object_getClass,用obj->isa兜底取类。
这正是 README 所说"在 Mac OS X v10.3 到 iOS 2.0+ 全部版本都能工作"的原因。
第 1 步:查找两个方法(L34–L52)
用class_getInstanceMethod分别取出原始方法和替身方法的Method结构体。任何一个查不到,就通过 SetNSError 宏 写入带函数名和描述的错误信息并返回NO——先校验、后动手,保证不会留下"改了一半"的脏状态。
第 2 步:关键技巧——把方法"提升"到目标类(L54–L61)⭐
这是整个库最精髓的地方,源码只有两次class_addMethod:
class_addMethod(self, origSel_, class_getMethodImplementation(self, origSel_), method_getTypeEncoding(origMethod)); class_addMethod(self, altSel_, class_getMethodImplementation(self, altSel_), method_getTypeEncoding(altMethod));class_addMethod的特性是:类上如果已有该方法,什么都不做;没有,才添加。
所以这两行代码的实际效果是:
- 方法本来就是目标类自己定义的→ 无操作,直接通过;
- 方法是从父类继承来的 → 把它复制一份(连同 IMP 和类型编码)挂到目标类上,变成"类自己的方法"。
这一步叫"方法提升(hoisting)"。它保证了第 3 步交换的永远是目标类自己的两份方法记录,绝不会误伤父类和其它兄弟子类。
第 3 步:交换实现(L63)
method_exchangeImplementations(class_getInstanceMethod(self, origSel_), class_getInstanceMethod(self, altSel_));到这里,两个方法已确定都"住"在同一个类的方法列表里,可以放心交给method_exchangeImplementations完成对调,返回YES。
五、method_exchangeImplementations 底层机制剖析 🔬
这个运行时 API 的语义非常纯粹:把两个Method记录里的method_imp字段互换,仅此而已。它不修改方法名、不改类型编码、不动方法列表结构。
理解它有三个要点:
- 前提是"同一类"——两个方法必须位于同一个类(或元类)的方法列表中,否则行为未定义。这就是为什么 JRSwizzle 要先做第 2 步的"提升";
- 交换的是实现,不是名字——交换后
[obj foo]依然按foo的签名去调用,只是执行到了my_foo的函数体,所以两边签名不一致时仍有风险; - 它是"原子级"的安全原语——只做一次指针互换,比手动改写
method_imp少出错,这也是 macOS 10.5 / iOS 2.0 之后官方推荐的做法。
六、旧版兼容路径:手动交换的 Ballard 实现 🛠️
对于OBJC_API_VERSION < 2的老系统(没有method_exchangeImplementations),JRSwizzle.m 走一条完全手工的路径,致敬的是 Kevin Ballard 的经典实现:
- 遍历方法列表(L70–L84):用
class_nextMethodList只找目标类"直接拥有"的方法,刻意排除继承方法; - 提升继承方法(L112–L127):缺哪个就用
class_getInstanceMethod从继承链取出,手工拼一个objc_method_list再class_addMethods挂上去——注释里还特意把obsolete字段置空来"安抚 valgrind"; - 手动交换(L130–L132):
IMP temp = directOriginalMethod->method_imp; directOriginalMethod->method_imp = directAlternateMethod->method_imp; directAlternateMethod->method_imp = temp;三步逻辑与现代路径完全同构:先提升,再交换。测试目录里的 MethodSwizzle.m 保留了这一实现的完整对照版本,可结合阅读。
七、类方法与 Block 注入:进阶 API 🚀
类方法交换只有一行(JRSwizzle.m):
return [GetClass((id)self) jr_swizzleMethod:origSel_ withMethod:altSel_ error:error_];原理一句话:类方法是元类(metaclass)的实例方法。GetClass((id)self)取出元类后,类方法交换就退化成了普通的实例方法交换。
Block 注入(JRSwizzle.m)则更巧妙,分四步:
imp_implementationWithBlock把 Block 包装成一个 IMP;- 用方法名 + Block 地址拼出唯一选择器(如
_jr_block_foo_0x1005),避免重名; class_addMethod把 Block 方法挂上类,再走标准流程与原方法交换;- 返回一个
NSInvocation,在 Block 内部可以调它来执行原始实现——实现"执行我的逻辑后再补刀原方法"的经典模式。
注意 JRSwizzle.h 的注释示例:Block 里通过__block NSInvocation *invocation引用自身,先打日志、再invoke原方法、最后取返回值。版本历史显示该 API 是 v1.1.0 加入的,作者也坦承NSInvocation不是最快的路径,性能敏感场景请谨慎。
八、测试用例如何验证"继承也正确" ✅
JRSwizzleTest.m 里两个场景测试,恰好对应 README 对比表中的第 7、8 行(其余实现各有一行是 NO):
- 场景 7(直接实现):父类
A7定义foo7,子类B7重写了它,类别里加了altFoo7。对B7交换后——b foo7走altFoo7,而父类实例a foo7完全不受影响(L41-L78); - 场景 8(继承实现):
B8没有重写foo8,是从父类继承的。交换后B8走altFoo8,A8依然走原foo8(L114-L151)。
第二个场景就是"提升"技巧的用武之地:foo8本来在A8的方法列表里,先被复制到B8再交换,所以父类安然无恙。两个测试断言里那句// CORRECT BEHAVIOR注释,正是整个库的立身之本。
九、上手指南:如何安装使用 📥
方式一:CocoaPods(推荐)
根据 JRSwizzle.podspec,直接:
pod 'JRSwizzle'方式二:手动引入
clone 仓库后,把 JRSwizzle.h 和 JRSwizzle.m 加入工程即可:
git clone https://gitcode.com/gh_mirrors/jr/jrswizzle使用建议:把交换调用放在+load或+initialize中执行,且只交换一次——重复交换等于把实现换回去,是最常见的自伤操作。
十、总结:一张表看懂全部机制 🎯
| 机制 | 对应源码 | 一句话解释 |
|---|---|---|
| 查找方法 | L34-L52 | 先校验,找不到就报 NSError,绝不留脏状态 |
| 方法提升 | L54-L61 | 两次class_addMethod,把继承方法复制成"类自己的" |
| 实现交换 | L63 | method_exchangeImplementations只互换 IMP 指针 |
| 旧系统兼容 | L66-L135 | 无该 API 时手工"提升 + 手动交换" |
| 类方法交换 | L138-L140 | 取元类后委托给实例方法交换 |
| Block 注入 | L142-L164 | Block → IMP → 挂类 → 交换,NSInvocation 保留原实现 |
JRSwizzle 的源码不长,却把方法交换的每个坑都填平了:先提升、再交换的六字心法,配合完善的错误诊断和跨版本兼容,让它成为学习 Objective-C 运行时方法交换机制的最佳"活教材"。读懂这 200 行代码,你就真正掌握了method_exchangeImplementations的全套使用姿势。
【免费下载链接】jrswizzleone-stop-shop for all your method swizzling needs项目地址: https://gitcode.com/gh_mirrors/jr/jrswizzle
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考