从底层实现的角度来看,weak 引用虽然是解决循环引用的安全利器,但它并非“免费”的。其开销主要体现在 运行时性能、内存布局以及开发中的逻辑限制 三个方面。
以下是详细的拆解:
1. 运行时的性能开销(CPU)
weak 引用在访问时比 strong 或 unowned 慢,原因在于它涉及多层间接查找:
- 间接寻址(Side Table 查找):
weak指针并不直接指向对象,而是指向一个 Side Table(侧表) 。每次读取weak变量时,CPU 必须先跳转到侧表内存,读取侧表中的强引用计数,再根据地址跳转到真实对象。这种两次跳转会增加缓存失效(Cache Miss)的概率。 - 读取时的原子操作: 为了保证线程安全,在读取
weak引用时,运行时(Runtime)需要调用swift_weakLoadStrong。这个函数内部会进行原子操作,检查对象是否已销毁。如果没销毁,它会临时增加强引用计数以确保你在使用期间对象不会被突然释放,随后再减少计数。这些原子操作在并发环境下会产生微小的竞争开销。 - 置 nil 的逻辑成本: 虽然
weak的“置 nil”是逻辑上的(即访问时才确定),但如果一个对象有大量的弱引用,当该对象彻底消失、侧表也需要被回收时,系统仍需维护这些引用计数的状态机流转。
2. 内存空间的开销(Memory)
-
侧表分配: 当一个对象第一次被
weak引用时,系统必须在堆上为其分配一个侧表空间(约几十个字节)。如果你的程序中有数百万个小对象都被弱引用,这笔额外的堆内存分配是不容忽视的。 -
内存释放的延迟: * 对象内存: 强引用归零时,对象的物理内存会被回收。
- 侧表内存: 即使对象已经销毁,只要还有一个
weak指针指向该侧表,侧表本身就不会被回收。这意味着weak引用的存在会稍微延长“管理元数据”所占用的内存寿命。
- 侧表内存: 即使对象已经销毁,只要还有一个
3. 开发中的逻辑与语法限制
-
强制可选性(Optional):
weak变量必须声明为Optional。这意味着你每次使用它时都必须进行解包(if let或guard let),这增加了代码的冗余。 -
必须是变量(var):
weak不能声明为let。因为根据定义,它在运行期间随时可能被运行时系统改变(变为nil),这违反了let的不可变性。 -
类型限制: *
weak只能引用**类(Class)**实例或满足AnyObject约束的协议。你不能对Struct或Enum使用weak,因为它们是值类型,没有引用计数和侧表机制。- 不能在 非类类型对象(如纯 Swift 协议,除非标注为
: AnyObject)上直接使用,否则编译器会报错。
- 不能在 非类类型对象(如纯 Swift 协议,除非标注为
4. 线程安全中的“微妙”陷阱
虽然 weak 引用本身在读取和置空时是线程安全的,但它不保证你的业务逻辑是线程安全的。
例如:
Swift
if weakObject != nil {
// 在这里执行时,weakObject 仍可能在另一个线程被释放
weakObject!.doSomething() // 依然可能崩溃
}
正确做法: 必须使用 if let strongObject = weakObject 进行“强持有”后再操作。这会触发上文提到的原子操作开销。
总结:weak vs unowned 的开销对比
| 维度 | weak | unowned |
|---|---|---|
| 内存结构 | 必须分配侧表 | 依然内联在对象内存中 |
| 访问速度 | 较慢(涉及侧表和解包) | 快(接近强引用) |
| 内存回收 | 侧表会留存至弱引用消失 | 对象物理内存留存(僵尸态)至无主引用消失 |
| 安全性 | 绝对安全(自动置 nil) | 风险较高(对象销毁后访问会崩溃) |
建议: 在高性能的 UI 渲染循环(如 draw 方法或每秒 60 帧的逻辑)中,过度使用 weak 可能会累积可察觉的性能下降。此时,如果能通过架构设计保证生命周期(例如父子关系明确),使用 unowned 或改进持有关系会是更好的选择。
英文版
8-10. [Memory Management] What are the overheads and limitations of weak references?
From a low-level perspective, while weak references are a vital safety tool for breaking retain cycles, they are not "free." Their costs manifest in three primary areas: runtime performance (CPU), memory layout, and development logic constraints.
1. Runtime Performance Overhead (CPU)
A weak reference is slower to access than a strong or unowned reference because it involves multiple layers of indirect lookup:
- Indirect Addressing (Side Table Lookup): A
weakpointer does not point directly to the object; it points to a Side Table. Every time you read aweakvariable, the CPU must first jump to the Side Table memory, read the strong reference count, and then jump to the actual object's address. This "double hop" increases the probability of a Cache Miss. - Atomic Operations on Read: To ensure thread safety, the runtime calls
swift_weakLoadStrongduring access. This function performs atomic operations to check if the object has been destroyed. If it's alive, it temporarily increments the strong reference count to ensure the object isn't released while you are using it, then decrements it afterward. These atomic instructions incur micro-overheads, especially in high-concurrency environments. - Logical Cost of Nil-ing: While "zeroing" a
weakreference is logical (determined at access time), the system still needs to manage the state machine transitions for the Side Table when an object with many weak references is finally destroyed.
2. Memory Space Overhead (Memory)
-
Side Table Allocation: When an object is first referenced weakly, the system must allocate space for a Side Table on the heap (typically a few dozen bytes). If your application has millions of small objects that are all weakly referenced, this cumulative heap allocation becomes non-negligible.
-
Delayed Memory Reclamation:
- Object Memory: When the strong count hits zero, the object's physical memory is reclaimed.
- Side Table Memory: Even after the object is destroyed, as long as a single
weakpointer still points to that Side Table, the Side Table itself cannot be reclaimed. This meansweakreferences slightly extend the lifespan of "management metadata" in memory.
3. Development Logic and Syntax Limitations
-
Mandatory Optionality: A
weakvariable must be declared as anOptional. This forces you to unwrap it (if letorguard let) every time you use it, which can lead to more verbose code. -
Must be a Variable (
var): Aweakreference cannot be aletconstant. By definition, its value can be changed by the runtime system (transitioning tonil) at any point, which violates the immutability oflet. -
Type Restrictions:
weakcan only reference Class instances or protocols constrained toAnyObject. You cannot useweakonStructsorEnumsbecause they are value types and lack the reference counting and Side Table infrastructure.- It cannot be used directly on non-class protocol types (unless marked with
: AnyObject), or the compiler will throw an error.
4. The "Subtle" Thread Safety Trap
While the act of reading and zeroing a weak reference is thread-safe at the runtime level, it does not guarantee that your business logic is thread-safe.
Consider this unsafe pattern:
Swift
if weakObject != nil {
// By the time this line executes, weakObject might have been
// released by another thread!
weakObject!.doSomething() // Potential Crash
}
The Correct Approach: Always use if let strongObject = weakObject to create a "strong handle" before performing operations. This triggers the atomic overhead mentioned earlier but ensures the object stays alive for the duration of the scope.
Summary: Overhead Comparison (weak vs. unowned)
| Dimension | weak | unowned |
|---|---|---|
| Memory Structure | Requires external Side Table allocation | Inline within the object's memory |
| Access Speed | Slower (Side table + Optional unwrapping) | Fast (Comparable to strong reference) |
| Memory Reclamation | Side Table persists until weak count is 0 | Object physical memory persists (Zombie state) until unowned count is 0 |
| Safety | Absolute safety (Auto-nil-ing) | Higher risk (Crashes if accessed after deallocation) |