12-9.【OC】【Swift+OC 混编】OC 调用 Swift 方法时,是否存在 retain / release 异常?

5 阅读3分钟

这是一个非常敏锐的问题。简单来说:在正常桥接(@objc)的情况下,不会出现 retain/release 异常

这是因为 Swift 编译器在生成 Thunk(存根函数) 时,已经通过一套名为 “ARC 语义转换” 的机制,完美地平衡了 Objective-C 和 Swift 在内存管理约定上的微小差异。


1. 核心冲突:谁负责平衡引用计数?

虽然 OC 和 Swift 都使用 ARC,但它们在“调用方(Caller)”和“被调用方(Callee)”的责任分配上略有不同:

  • Objective-C 约定:遵循 CamelCase 命名约定。对于非 alloc/new/copy 开头的方法,被调用方(Swift 实现)返回的对象通常是 autorelease 的,或者调用方(OC)默认不持有该返回值的强引用。
  • Swift 原生约定:为了性能,Swift 内部通常通过寄存器直接传递强引用,并由调用方决定何时释放。

编译器如何修复冲突?

当你从 OC 调用一个 @objc 的 Swift 方法时,你实际上调用的是 Thunk 函数。这个 Thunk 扮演了“ARC 翻译官”的角色:

  1. 参数保护:Thunk 会对传入的 OC 对象执行 retain,确保在 Swift 执行期间对象不会被销毁。
  2. 返回值处理:如果 Swift 方法返回一个对象,Thunk 会根据 OC 的规范,在返回前对该对象执行 objc_retainAutoreleaseReturnValue。这样 OC 端就能按照其熟悉的 autorelease 逻辑正常处理。

2. 容易出现异常的“重灾区”

虽然编译器做了很多工作,但在以下三种高级场景中,确实可能出现引用计数异常:

A. 不安全的指针转换 (UnsafePointer)

如果你在协议或方法中传递了 UnsafeRawPointerCOpaquePointer,编译器会跳过所有的 ARC 自动注入。

  • 后果:如果你在 Swift 端释放了对象,但 OC 端还在使用该指针,会直接导致 Use-after-free 崩溃。

B. Block 与 Closure 的循环引用

这是最常见的“异常”。

  • 当 OC 传递一个 Block 给 Swift,Swift 用强引用持有了这个 Block,而 Block 内部又捕获了 Swift 对象时,会形成 循环引用(Retain Cycle)
  • 差异:OC 不支持 Swift 的 [weak self] 捕获列表语法。你必须在 OC 端手动创建 __weak 指针再传入。

C. NS_RETURNS_RETAINED 语义误判

如果你通过 @objc(name) 手动重命名方法,且新名字以 allocnew 开头,OC 编译器会认为该方法返回的对象已经执行过 retain

  • 风险:如果 Swift 端没有同步调整语义,OC 可能会多执行一次 release,导致提前释放;或者少执行一次 release,导致内存泄漏。

3. 性能影响:AutoreleasePool 的堆积

虽然不会“崩溃”,但 OC 调用 Swift 可能会导致 内存峰值波动

  • 由于桥接层为了兼容 OC,经常会将 Swift 返回的对象放入 AutoreleasePool
  • 如果在循环中大量从 OC 调用 Swift 方法,这些对象不会立即释放,直到当前的 runloop 结束。

4. 如何排查这类异常?

如果你怀疑存在内存异常,可以使用以下工具:

  1. Instruments - Leaks/Allocations:查看引用计数的完整生命周期。
  2. objc_setAssociatedObject 调试:在 Swift 对象上关联一个检测对象,看它在 OC 端逻辑结束时是否触发了 deinit
  3. Xcode 环境变量:开启 NSZombieEnabled 来定位过度释放(Over-release)的问题。

💡 深度启发

在 Swift ABI 稳定后,这种桥接变得更加高效。Swiftc 编译器现在能更好地识别“立即被销毁”的对象,从而跳过不必要的 autorelease 操作。