8-3.【内存管理机制】Swift 与 Objective-C 的 ARC 有哪些差异?

26 阅读5分钟

虽然 Swift 的 ARC 在概念上继承自 Objective-C,但在底层实现、性能优化以及处理边缘情况的方式上,两者有着显著的代差。

你可以将 Objective-C 的 ARC 看作是 “基于运行时函数的管理” ,而 Swift 的 ARC 则是 “深度集成于编译器与静态类型的管理”


1. 引用计数存储位置:Runtime Table vs. Inline Bitfield

这是两者最大的硬件层级差异:

  • Objective-C:绝大多数对象的引用计数存储在全局的 Side Tables(散列表)中。当你 retain 一个对象时,系统会对对象的地址进行哈希运算,去全局表中查找对应的计数器并加锁修改。

  • Swift:引用计数默认**内联(Inline)**在对象头的 HeapObject 中。

    • 优势:由于计数器就在对象内存里,CPU 缓存命中率极高,不需要访问全局表,避免了全局锁竞争。只有在出现弱引用或计数溢出时,Swift 才会按需创建侧表。

2. 弱引用实现机制:自动置空 (Zeroing Weak)

  • Objective-C:当对象销毁时,运行时系统会遍历全局弱引用表,手动将指向该地址的所有 __weak 指针置为 nil。这是一个相对沉重的运行时操作。

  • Swift:采用 “逻辑置空” 结合侧表。

    • 当一个 Swift 对象被销毁但仍有弱引用时,侧表(Side Table)会继续存在。
    • 当弱引用再次被访问时,它会先检查侧表里的“强引用计数”。如果发现为 0,则直接返回 nil 并清理自己。这种方式将清理工作分散到了访问时,而不是集中在销毁时。

3. 无主引用 (Unowned) 的引入

Objective-C 只有 __weak__unsafe_unretained

  • __unsafe_unretained 在对象销毁后会变成野指针,非常危险。

Swift 引入了 unowned

  • 它不增加强引用计数,但它在底层会增加一个 Unowned 引用计数
  • 安全性:当对象被销毁后,如果你尝试访问 unowned 引用,Swift 运行时会检查 Unowned 计数并直接抛出运行时错误(Trap),而不是像 Obj-C 那样任由程序访问非法内存导致静默崩溃或安全漏洞。

4. 编译器优化策略

  • Objective-C:主要依赖 objc_retainobjc_release 的运行时调用。虽然有 ARC,但编译器很难做跨函数的深层次分析。

  • Swift (SIL 优化) :Swift 拥有 SIL (Swift Intermediate Language) 层。

    • 编译器能看穿闭包的捕获列表,并进行 ARC 消除(ARC Elision)
    • 借用检查(Borrowing) :Swift 5.9 引入了明确的消耗(Consuming)和借用(Borrowing)概念。如果一个对象只是作为参数传递给函数且不被存储,编译器可以完全不生成 retain/release 指令,这在 Obj-C 中很难实现。

5. 总结对比表

特性Objective-C ARCSwift ARC
计数存储全局 Side Tables (散列表)对象头部内联位域 (Inline Bitfield)
性能较慢(涉及全局查找和锁)极快(寄存器操作与缓存友好)
弱引用清理销毁时主动扫描置空侧表记录,访问时逻辑置空
安全性unsafe_unretained 易导致野指针unowned 提供运行时崩溃保护
静态分析相对有限极强(结合 SIL 和所有权模型)

为什么这些差异对你很重要?

在 Objective-C 中,频繁操作引用计数(比如在很长的 for 循环中)的性能代价非常大,开发者通常需要手动添加 @autoreleasepool。而在 Swift 中,由于内联计数和强大的编译器优化,这种性能损耗被降到了最低,大部分场景下你完全不需要感知引用的发生。

英文版

8-3. [Memory Management] What are the differences between Swift and Objective-C ARC?

While Swift's ARC conceptually inherits from Objective-C, there is a significant generational gap in their underlying implementation, performance optimization, and handling of edge cases.

You can think of Objective-C's ARC as "Runtime-Function-Based Management," whereas Swift's ARC is "Deeply Integrated Compiler and Static-Type Management."


1. Storage of Reference Counts: Side Tables vs. Inline Bitfield

This is the most fundamental difference at the hardware/memory level:

  • Objective-C: For the vast majority of objects, reference counts are stored in global Side Tables (hash tables). When you retain an object, the system performs a hash of the object's address, looks up the corresponding counter in a global table, and modifies it under a lock.
  • Swift: Reference counts are Inlined directly into the object's header (HeapObject) by default.
  • Advantage: Since the counter resides within the object's own memory, CPU cache hit rates are extremely high. It eliminates the need to access global tables and avoids contention for global locks. Swift only creates a Side Table on demand when a weak reference is created or if the inline counter overflows.

2. Weak Reference Mechanism: Zeroing Weak

  • Objective-C: When an object is deallocated, the runtime system traverses a global weak reference table and manually sets all __weak pointers pointing to that address to nil. This is a relatively heavy runtime operation concentrated at the moment of destruction.
  • Swift: Employs "Logical Zeroing" combined with Side Tables.
  • When a Swift object is destroyed but still has weak references, the Side Table remains in existence.
  • When a weak reference is subsequently accessed, it first checks the "Strong Reference Count" in the Side Table. If it finds the count is 0, it immediately returns nil and cleans itself up. This approach distributes the cleanup work to the moment of access rather than concentrating it all during deallocation.

3. Introduction of Unowned References

Objective-C only offers __weak and __unsafe_unretained.

  • __unsafe_unretained becomes a dangling pointer after the object is destroyed, which is highly dangerous.

Swift introduced unowned:

  • It does not increment the strong reference count, but it increments an Unowned Reference Count in the metadata.
  • Safety: When an object is destroyed, if you attempt to access an unowned reference, the Swift runtime checks the Unowned count and immediately triggers a runtime error (Trap). This prevents the silent crashes or security vulnerabilities associated with accessing invalid memory as seen in Objective-C.

4. Compiler Optimization Strategies

  • Objective-C: Relies heavily on runtime calls to objc_retain and objc_release. Despite having ARC, the compiler struggles with deep cross-functional analysis.
  • Swift (SIL Optimization) : Swift utilizes SIL (Swift Intermediate Language) .
  • The compiler can "see through" closure capture lists and perform ARC Elision.
  • Borrowing Check: Swift 5.9 introduced explicit Consuming and Borrowing concepts. If an object is simply passed as a parameter to a function and not stored, the compiler can omit retain/release instructions entirely—a feat that is difficult to achieve in Objective-C.

5. Summary Comparison Table

FeatureObjective-C ARCSwift ARC
Count StorageGlobal Side Tables (Hash Tables)Inline Bitfield in Object Header
PerformanceSlower (Global lookups and locks)Extremely Fast (Register ops & cache-friendly)
Weak CleanupActive scan and zeroing at deallocationSide Table tracking, logical zeroing at access
Safetyunsafe_unretained leads to dangling pointersunowned provides runtime trap protection
Static AnalysisRelatively limitedPowerful (Integrated with SIL and Ownership)

Why these differences matter to you?

In Objective-C, the performance cost of frequent reference count operations (e.g., inside a long for loop) is quite high, often requiring developers to manually add @autoreleasepool. In Swift, due to inline counting and aggressive compiler optimizations, this performance overhead is minimized. In most scenarios, you won't even perceive the reference counting overhead occurring.