2-21.【OC】【内存管理】Swift ARC 与 OC ARC 在循环引用处理上的设计差异有哪些?

0 阅读3分钟

虽然两者都基于自动引用计数(ARC) ,但 Swift 在设计上吸取了 Objective-C 的教训,将内存安全性从“开发者规约”提升到了“语言特性”层面。

它们处理循环引用的核心设计差异可以总结为以下四点:


1. 语法集成度:捕获列表 (Capture List) vs. 外部声明

这是开发中最直观的差异。

  • Objective-C (手动破坏性声明)

    为了打破 Block 的循环引用,你必须在 Block 外部手动声明一个弱引用变量。

    Objective-C

    __weak typeof(self) weakSelf = self;
    self.block = ^{
        [weakSelf doSomething];
    };
    
  • Swift (内联式声明)

    Swift 引入了捕获列表,允许在闭包的起始位置直接定义捕获规则。这种方式不仅代码整洁,更重要的是它将“如何捕获”变成了闭包定义的一部分。

    Swift

    self.closure = { [weak self] in
        self?.doSomething()
    }
    

2. 弱引用类型的丰富度:Unowned 的引入

Objective-C 只有一种安全的弱引用:__weak。而 Swift 区分了两种非强引用:

  • weak:与 OC 的 __weak 类似,安全、自动置 nil、必须是可选型。

  • unowned (Swift 独有) :类似于 OC 的 __unsafe_unretained,但不完全相同。

    • 设计初衷:用于解决两个对象“同生共死”且无需处理 Optional 时的性能优化。
    • 底层保护unowned 会增加一个特殊的 Unowned 引用计数。虽然访问释放后的 unowned 会崩溃,但这种崩溃是“确定性”的(Runtime 陷阱),而不是 OC 那种随机的“野指针”非法访问。

3. 底层实现:Side Table (侧表) 的创建时机

在 Objective-C 中,__weak 对 Side Table 的依赖非常重。而 Swift 进行了性能优化:

  • Objective-C:一旦对象被弱引用,系统就会在全局哈希表中寻找或创建 Side Table。

  • Swift:对象最初只有 Strong 和 Unowned 计数。只有当对象第一次被 weak 引用时,才会按需(On-demand)从内部引用计数空间“溢出”到一个外部的 Side Table

    • 这种设计让那些没有弱引用的对象在内存排布上更加紧凑,读写效率更高。

4. 变量捕获的默认语义

正如我们之前讨论的,这是最容易坑到 OC 转 Swift 开发者的地方:

  • OC Block:默认捕获变量的值 (Value) 。如果不加 __block,你在 Block 里改不了外部变量,也感知不到外部的变化。
  • Swift Closure:默认捕获变量的引用 (Reference) 。这意味着 Swift 闭包默认比 OC Block 更容易通过“共享状态”意外产生循环引用(即便不涉及 self,两个闭包捕获同一个 var 变量也可能形成链条)。

5. 编译器检查的严苛程度

Swift 编译器比 OC 编译器“唠叨”得多:

  • OC:如果你在 Block 里直接写 self,编译器通常只给一个淡淡的 Warning(或者干脆不给)。
  • Swift:如果你在闭包里使用 self 而没有解包或明确捕获关系,且该闭包是逃逸的(@escaping),编译器有时会强制要求你写出 self.,作为一种“死亡提醒”,强迫你意识到这可能是一个循环。

总结对照表

特性Objective-C ARCSwift ARC
打破循环语法外部声明 __weak内部捕获列表 [weak self]
非强引用选项__weak, __unsafe_unretainedweak, unowned
对 nil 的处理自动置 nil (安全)weak 置 nil / unowned 触发断言崩溃
内存结构全局 SideTable 映射对象内计数 \to 溢出到 SideTable
变量捕获语义默认值捕获默认引用捕获