Objective-C Runtime:从消息派发到安全 Hook

Objective-C Runtime:从消息派发到安全 Hook

这套笔记不把 Runtime 当成一组需要背诵的 C API,而是沿着"对象怎样表示、消息怎样找到实现、找不到后怎样转发、框架怎样改变这条链"逐层建立心智模型。

目录

  1. Runtime 是什么
  2. 对象、类对象与元类
  3. SEL、Method、IMP、Ivar 与 Property
  4. 消息发送与 super
  5. 三阶段消息转发
  6. 动态类和动态子类
  7. Method Swizzling
  8. KVO
  9. Associated Object
  10. Runtime AOP
  11. Aspects 为什么风险较高
  12. RSSwizzle 为什么相对安全
  13. 生产边界与测试策略
  14. 与 DelegateProxy 的关系

知识地图

总图只承担导航,不把每个 API 塞进去:

mindmap root((Runtime)) 对象模型 Class 与 Meta-Class 消息机制 查找与转发 动态能力 动态类与 KVO Hook IMP Swizzle 转发 AOP 工程安全 ABI 继承与多方冲突

案例索引、操作步骤和独立文件见工程内 03-runtime/CASES.md

一、Runtime 是什么

Objective-C 编译器不会把普通方法调用完全固定成一个函数地址。[receiver doWork] 会被编译为消息发送,运行时根据 receiver 当前的 Class、selector 和继承关系决定执行哪个 IMP。这使分类、动态解析、消息转发、KVO 和 Method Swizzling 成为可能。

学习时要区分三层边界:Objective-C 语言语义、<objc/runtime.h> 等公开接口、系统内部实现。方法缓存的存在可以帮助理解性能,但私有缓存结构不是业务代码可依赖的契约。

二、对象、类对象与元类

  • 实例的 isa 指向类对象,类对象保存实例方法。
  • 类对象自身也是对象,它的 isa 指向元类;元类保存类方法。
  • superclass 形成继承查找链;根元类的 isa 最终闭合到根元类。
  • 现代 Runtime 的 isa 可能包含压缩和标记位,不应该把它当成普通裸指针直接位运算。

object_getClass(object) 读取真实运行时 Class;[object class] 是一条可被重写的消息。KVO 正是利用二者差异,在改变 isa 后通过重写 -class 隐藏动态子类。

对应实验还会分别枚举 Ivar 与 Property。只读计算属性可以有 Property 元数据和 getter,却没有同名实例存储;自动合成属性则通常产生带下划线的 Ivar。两份列表不能互相替代。

三、SEL、Method、IMP、Ivar 与 Property

  • SEL 是方法名字的唯一标识,不包含具体实现。
  • Method 是 Runtime 方法记录,可查询 selector、IMP 和类型编码。
  • IMP 是可调用函数地址,至少接收 self_cmd 两个隐式参数。
  • Ivar 描述真实实例布局;Property 主要描述声明元数据和访问器约定,不等于一定存在同名 Ivar。

调用 IMP 时函数指针签名必须与方法类型编码一致。参数数量、结构体返回规则或浮点 ABI 不一致都属于未定义行为,不能因为一次运行没崩就认为安全。

四、消息发送与 super

概念上的查找流程:

flowchart LR M[receiver + selector] --> C[当前 Class] C --> Cache[查询缓存] Cache -->|命中| IMP[调用 IMP] Cache -->|未命中| List[查方法列表] List -->|没有| Parent[沿 superclass] Parent -->|全链未找到| Forward[进入解析与转发]

super 不是另一个对象。它仍把 self 作为 receiver,只把方法查找起点改为编译期父类。因此父类实现内部看到的 self 仍是原来的子类实例。

五、三阶段消息转发

未找到 IMP 后依次有三次机会:

  1. resolveInstanceMethod: / resolveClassMethod: 动态补充实现。
  2. forwardingTargetForSelector: 快速换一个接收者。
  3. methodSignatureForSelector:forwardInvocation: 进入完整转发。

完整转发依赖正确的 NSMethodSignature。统一伪造 v@: 会丢失参数和返回值布局。DelegateProxy 能透明传递未知代理方法,也正是因为它从真实下游获取签名并转发同一个 invocation。

六、动态类和动态子类

objc_allocateClassPair 创建类后,可以在注册前添加 Ivar 和方法;调用 objc_registerClassPair 后类布局固定。单实例 Hook 则创建目标类的动态子类,再用 object_setClass 只改变一个实例。

这类方案比全局 Swizzle 作用域小,但仍需处理:类名冲突、重复安装、原 Class 保存、恢复时机,以及对象已经被 KVO 或其他框架动态子类化的情况。不能看到动态类就强行拆链。

七、Method Swizzling

最常见错误是对子类调用 class_getInstanceMethod 后直接 method_exchangeImplementations。如果子类没有覆盖该方法,取到的 Method 实际属于父类,交换会污染所有父类实例和兄弟子类。

安全的基本步骤是:

  1. 找到当前继承链可见的 Method、IMP 和类型编码。
  2. class_addMethod 把该实现物化到目标子类。
  3. 只替换目标类自己的方法槽位。
  4. 用唯一安装键和锁保证同一 Hook 不会重复安装。
  5. 在安装锁内先取得上一层 IMP,由 factory 创建已经捕获它的 replacement,再把新 IMP 发布到 Class。

如果采用"先替换方法,再用输出参数把 original IMP 写到外部全局变量"的接口,并发线程可能在两步之间进入 replacement,此时 original 仍为空,会直接崩溃。factory/provider 的价值不只是 API 好看,而是消除这个发布窗口。replacement 调用 original 时仍必须使用完全匹配 ABI 的函数指针。

这里的锁只保护"安装过程",不自动保证被 Hook 方法内部的业务状态线程安全。

并发实验让 32 个线程使用同一 Class、selector 和唯一键竞争安装,只有一个调用能够成功。再次运行成功数为 0 是幂等结果,而不是失败;Method Hook 是进程级 Class 修改,不能为了刷新 UI 任意卸载和重装。

八、KVO

KVO 通常为被观察对象创建动态子类,改变实例 isa,在子类 setter 中发送变化通知,并重写 -class 报告原类。实验只验证公开可观察结果,不依赖 NSKVONotifying_ 私有命名规则。

automaticallyNotifiesObserversForKey: 对某个 key 返回 NO,setter 必须用成对的 willChangeValueForKey: / didChangeValueForKey: 包围真正赋值。派生属性可以用依赖键声明传播变化;这和 KVO 自动生成动态子类是两个互补机制。

动态子类不是 DelegateProxy 的必要条件。KVO 需要拦截某个实例自身的 setter;DelegateProxy 已经占据组件公开的 delegate 槽位,可以通过消息转发完成职责链,两者解决的问题不同。

九、Associated Object

关联对象让分类或 Hook 框架给现有对象附加状态。常用策略对应 assign、retain、copy 及 atomic/nonatomic 变体。它管理的是关联值所有权,不是业务级线程安全:一次 get 后修改可变集合仍需要自己的同步方案。

assign 不保留对象,也不是自动清零 weak。若被关联对象先释放,继续读取可能得到悬空引用,因此生产代码通常不把 assign 用作对象弱引用替代品。

十、Runtime AOP 的两条路线

IMP 路线

把明确 selector 的 IMP 换成 replacement,replacement 调用安装时捕获的 previous IMP。多个 Hook 可以形成显式嵌套链,但调用顺序仍取决于安装顺序。

消息转发路线

先把原 IMP 保存到 alias selector,再把目标 selector 指向 _objc_msgForward,最后在 forwardInvocation: 中执行 before/instead/after 并调用 alias。

sequenceDiagram participant Caller participant Runtime participant Forward as forwardInvocation participant Alias as 原 IMP alias Caller->>Runtime: submit Runtime->>Forward: NSInvocation Forward->>Forward: before Forward->>Alias: invoke alias Alias-->>Forward: return value Forward->>Forward: after Forward-->>Caller: return value

十一、Aspects 为什么风险较高

先纠正一个表述:不是"使用 Aspects 一定不安全",而是它的 Hook 模型冲突面较大。

Aspects 类方案为了统一处理任意参数和返回值,会把 selector 导向消息转发,并维护 alias、Hook 容器、动态子类及 forwardInvocation:。风险集中在:

  • forwardInvocation: 是一个类级共享入口,原类、父类、KVO、代理框架或另一个 AOP 库都可能需要它。
  • alias selector 和转发 IMP 必须保持精确 ABI,任意一环错误都会破坏参数或返回值。
  • 继承方法与单实例动态子类同时存在时,Hook 究竟安装在哪一层会影响整个继承树。
  • 多方覆盖转发入口时,很难像普通 IMP 链一样可靠捕获"上一层"并继续调用。
  • selector 黑名单和系统内部调用关系会随系统版本变化;Hook 高频或关键 UIKit 生命周期方法的风险尤其高。

因此风险来自共享转发通道和复杂状态组合,不应简化为"因为项目老"或"因为用了 Runtime"。

十二、RSSwizzle 为什么相对安全

RSSwizzle 类型的封装主要围绕明确 selector 做 IMP 替换,并把容易写错的部分集中处理:

  • 正确区分目标类自有方法和继承方法。
  • 用唯一键避免相同 Hook 重复安装。
  • 串行化安装,避免两条线程同时读写方法槽位。
  • 通过 original IMP provider/factory 在发布 replacement 前捕获安装时的上一层实现,避免并发调用读到未初始化 original。
  • 尽量不占用 forwardInvocation:,因此与消息转发/KVO 的交叉面更小。

它只是相对安全,不是绝对安全。不同框架 Hook 同一 selector 仍有安装顺序依赖;任何一层漏调 original、调用两次、使用错误类型签名,都会改变行为。Apple 私有实现变化也不会因为套了一层库就消失。

十三、生产边界与测试策略

上线前至少验证:父类和兄弟子类不受污染、重复安装幂等、并发安装只有一次成功、原实现调用次数、多个 Hook 的确定顺序、返回值和 completion handler 只完成一次、KVO 前后组合、对象释放和动态子类恢复。

能通过组合、公开 delegate、通知或明确业务埋点解决的问题,优先不用全局 Hook。Runtime Hook 适合边界清晰、selector 稳定、测试充分且有明确收益的横切接入。

十四、与 DelegateProxy 的关系

  • Swizzle 改变某个类的方法实现,适合没有公开扩展点但必须横切普通消息的场景。
  • DelegateProxy 接住公开 delegate 槽位上的消息,再把 invocation 沿责任链传递,不需要改业务类自身实现。
  • KVO 创建动态子类拦截某个实例的 setter,用于属性变化通知。

三者都会使用 Runtime,但作用对象、所有权和冲突边界不同。不能因为实现底层都有消息派发,就把 DelegateProxy 解释成 KVO 或观察者模式。

运行

可以直接打开 RuntimeLab.xcodeproj,也可以打开外层 nodeOCAndSwift.xcworkspace,在 Pods 中查看并实时修改 RuntimeLab Development Pod 源码。

参考资料

  • Apple Objective-C Runtime Programming Guide
  • Apple Objective-C Runtime API Reference
  • Mike Ash, Method Replacement for Fun and Profit
  • steipete/Aspects 源码与 README 风险说明
  • rentzsch/jrswizzle 与 RSSwizzle 的继承安全 Hook 思路
相关推荐
GitLqr3 小时前
Flutter FocusNode 实战指南:玩转键盘焦点与用户输入体验
android·flutter·ios
星辰即远方20 小时前
天气预报总结
macos·ios·objective-c·cocoa
秋雨梧桐叶落莳1 天前
计算器仿写总结
学习·macos·ios·objective-c·cocoa
末代iOS程序员华仔1 天前
iOS上架海外工具类应用合规指南:避免封号与下架风险
flutter·ios·swift
末代iOS程序员华仔1 天前
Flutter 开发 iOS App 上架全流程与常见问题分析
flutter·ios
末代iOS程序员华仔1 天前
Flutter开发iOS海外工具类应用上架合规指南与风险规避
flutter·ios
AI云海2 天前
iOS 多 Target 打包、UAT/生产测试与上架 App Store 全流程指南
ios
阿里超级工程师2 天前
最新快速申请ios打包证书和profile文件步骤
ios·打包证书
2501_915909062 天前
iOS 应用反调试技详解术 检测调试器的原理与防护实践
android·ios·小程序·https·uni-app·iphone·webview