虽然 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_retain和objc_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 ARC | Swift 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
__weakpointers pointing to that address tonil. 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
niland 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_unretainedbecomes 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
unownedreference, 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_retainandobjc_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/releaseinstructions entirely—a feat that is difficult to achieve in Objective-C.
5. Summary Comparison Table
| Feature | Objective-C ARC | Swift ARC |
|---|---|---|
| Count Storage | Global Side Tables (Hash Tables) | Inline Bitfield in Object Header |
| Performance | Slower (Global lookups and locks) | Extremely Fast (Register ops & cache-friendly) |
| Weak Cleanup | Active scan and zeroing at deallocation | Side Table tracking, logical zeroing at access |
| Safety | unsafe_unretained leads to dangling pointers | unowned provides runtime trap protection |
| Static Analysis | Relatively limited | Powerful (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.