☰
JRSwizzle源码逐行解读:method_exchangeImplementations与方法交换全机制解析
2026/10/3 12:51:54 网站建设 项目流程

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.podspecCocoaPods 描述文件(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的特性是:类上如果已有该方法,什么都不做;没有,才添加。

所以这两行代码的实际效果是:

  1. 方法本来就是目标类自己定义的→ 无操作,直接通过;
  2. 方法是从父类继承来的 → 把它复制一份(连同 IMP 和类型编码)挂到目标类上,变成"类自己的方法"。

这一步叫"方法提升(hoisting)"。它保证了第 3 步交换的永远是目标类自己的两份方法记录,绝不会误伤父类和其它兄弟子类。

第 3 步:交换实现(L63)

method_exchangeImplementations(class_getInstanceMethod(self, origSel_), class_getInstanceMethod(self, altSel_));

到这里,两个方法已确定都"住"在同一个类的方法列表里,可以放心交给method_exchangeImplementations完成对调,返回YES。

五、method_exchangeImplementations 底层机制剖析 🔬

这个运行时 API 的语义非常纯粹:把两个Method记录里的method_imp字段互换,仅此而已。它不修改方法名、不改类型编码、不动方法列表结构。

理解它有三个要点:

  1. 前提是"同一类"——两个方法必须位于同一个类(或元类)的方法列表中,否则行为未定义。这就是为什么 JRSwizzle 要先做第 2 步的"提升";
  2. 交换的是实现,不是名字——交换后[obj foo]依然按foo的签名去调用,只是执行到了my_foo的函数体,所以两边签名不一致时仍有风险;
  3. 它是"原子级"的安全原语——只做一次指针互换,比手动改写method_imp少出错,这也是 macOS 10.5 / iOS 2.0 之后官方推荐的做法。

六、旧版兼容路径:手动交换的 Ballard 实现 🛠️

对于OBJC_API_VERSION < 2的老系统(没有method_exchangeImplementations),JRSwizzle.m 走一条完全手工的路径,致敬的是 Kevin Ballard 的经典实现:

  1. 遍历方法列表(L70–L84):用class_nextMethodList只找目标类"直接拥有"的方法,刻意排除继承方法;
  2. 提升继承方法(L112–L127):缺哪个就用class_getInstanceMethod从继承链取出,手工拼一个objc_method_list再class_addMethods挂上去——注释里还特意把obsolete字段置空来"安抚 valgrind";
  3. 手动交换(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)则更巧妙,分四步:

  1. imp_implementationWithBlock把 Block 包装成一个 IMP;
  2. 用方法名 + Block 地址拼出唯一选择器(如_jr_block_foo_0x1005),避免重名;
  3. class_addMethod把 Block 方法挂上类,再走标准流程与原方法交换;
  4. 返回一个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,把继承方法复制成"类自己的"
实现交换L63method_exchangeImplementations只互换 IMP 指针
旧系统兼容L66-L135无该 API 时手工"提升 + 手动交换"
类方法交换L138-L140取元类后委托给实例方法交换
Block 注入L142-L164Block → 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询