这是一个非常敏锐的问题。简单来说:在正常桥接(@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 翻译官”的角色:
- 参数保护:Thunk 会对传入的 OC 对象执行
retain,确保在 Swift 执行期间对象不会被销毁。 - 返回值处理:如果 Swift 方法返回一个对象,Thunk 会根据 OC 的规范,在返回前对该对象执行
objc_retainAutoreleaseReturnValue。这样 OC 端就能按照其熟悉的autorelease逻辑正常处理。
2. 容易出现异常的“重灾区”
虽然编译器做了很多工作,但在以下三种高级场景中,确实可能出现引用计数异常:
A. 不安全的指针转换 (UnsafePointer)
如果你在协议或方法中传递了 UnsafeRawPointer 或 COpaquePointer,编译器会跳过所有的 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) 手动重命名方法,且新名字以 alloc 或 new 开头,OC 编译器会认为该方法返回的对象已经执行过 retain。
- 风险:如果 Swift 端没有同步调整语义,OC 可能会多执行一次
release,导致提前释放;或者少执行一次release,导致内存泄漏。
3. 性能影响:AutoreleasePool 的堆积
虽然不会“崩溃”,但 OC 调用 Swift 可能会导致 内存峰值波动。
- 由于桥接层为了兼容 OC,经常会将 Swift 返回的对象放入
AutoreleasePool。 - 如果在循环中大量从 OC 调用 Swift 方法,这些对象不会立即释放,直到当前的 runloop 结束。
4. 如何排查这类异常?
如果你怀疑存在内存异常,可以使用以下工具:
- Instruments - Leaks/Allocations:查看引用计数的完整生命周期。
objc_setAssociatedObject调试:在 Swift 对象上关联一个检测对象,看它在 OC 端逻辑结束时是否触发了deinit。- Xcode 环境变量:开启
NSZombieEnabled来定位过度释放(Over-release)的问题。
💡 深度启发
在 Swift ABI 稳定后,这种桥接变得更加高效。Swiftc 编译器现在能更好地识别“立即被销毁”的对象,从而跳过不必要的 autorelease 操作。