在 Swift 闭包捕获列表中使用 [unowned self] 或 [weak self],本质上是在性能风险与安全性之间做抉择。
虽然它们都能打破循环强引用(Retain Cycle),但底层的处理逻辑和失败后果完全不同。
1. 核心区别对比
| 特性 | [weak self] | [unowned self] |
|---|---|---|
| 捕获类型 | 变成可选型 (Controller?) | 保持非可选型 (Controller) |
| 自动置 nil | 是。self 释放后,闭包内访问得到 nil | 否。self 释放后,指针地址依然残留 |
| 崩溃风险 | 零风险。配合 guard let 安全处理 | 高风险。若 self 已释放,访问即 崩溃 |
| 底层开销 | 较高。需操作侧表 (Side Table) 并进行可选解包 | 较低。直接访问内存,类似于强引用性能 |
| 语义假设 | “我不确定执行时你还在不在。” | “我确定执行时你一定还在。” |
2. 详细分析与风险
[weak self]:安全但略显繁琐
当闭包被捕获时,它不增加 self 的引用计数。
-
执行逻辑:闭包执行时,会尝试将
weak引用提升为强引用。如果self已经被释放,提升失败,返回nil。 -
代码风格:通常需要手动解包。
Swift
service.fetchData { [weak self] in guard let self = self else { return } // 安全守卫 self.updateUI() } -
风险:几乎没有内存层面的风险。唯一的“风险”是逻辑中断——如果闭包里的代码非常重要(如数据同步),而此时对象已销毁,后续逻辑将不再执行。
[unowned self]:高效但伴随悬垂指针
unowned 就像是在没有安全网的情况下高空作业。
-
执行逻辑:它假定对象在闭包调用期间始终存活。访问时,运行时会检查 Unowned 计数。
-
风险:运行时崩溃。如果闭包是一个异步回调(例如网络请求),而用户在请求返回前关闭了页面(对象已
deinit),此时闭包触发unowned self访问会直接抛出fatal error。Swift
service.fetchData { [unowned self] in // 如果 self 已经销毁,这里直接 Crash! self.updateUI() } -
底层细节:
unowned会让对象进入“僵尸状态”——虽然deinit执行了,但对象的物理内存在无主引用计数归零前不会被完全回收。
3. 场景选择指南
选 weak self 的情况(推荐 90% 的场景):
- 所有异步回调:网络请求、定时器(Timer)、复杂的 GCD 调度。
- 生命周期不明确:你不确定闭包什么时候被触发,也不确定触发时对象是否还在。
选 unowned self 的情况:
- 确定闭包与对象生命周期一致:例如,一个 View 内部的动画闭包,或者闭包本身就是对象初始化的一部分且不会逸出。
- 父子关系极其明确:例如,一个对象持有一个闭包,而这个闭包只会被该对象自己调用,且在对象销毁前一定会停止。
vs [unowned self] memory lifecycle diagram showing object deallocation and potential crash point]
4. 一个容易被忽略的风险:[weak self] 的提前释放
有时候你会发现,使用 [weak self] 导致闭包里的代码压根没跑完。
- 网络请求发起,
self(页面)被捕获。 - 用户点击“返回”,页面销毁,强引用归零。
- 网络返回,闭包执行,
guard let self = self失败,直接return。
如果闭包里有必须执行的清理逻辑(如写入磁盘、断开连接),使用 weak self 可能会导致这些逻辑被静默跳过。 此时应该审视:这个逻辑是否真的应该放在该对象的闭包里,还是应该剥离到专门的 Data Manager 中。
总结建议
- 新手/普通业务:无脑使用
[weak self]。多写一行guard let换取整个 App 的稳定性是绝对值得的。 - 追求性能/确定性关系:在确定闭包执行期间对象绝对存活的场景(如同步执行的
forEach闭包,虽然这种场景通常不需要捕获列表),可以使用unowned。
英文版
8-11. [Memory Management] What are the differences and risks of using unowned self and weak self in closures?
Using [unowned self] or [weak self] in a Swift capture list is essentially a choice between performance/risk and safety.
While both break strong reference cycles (Retain Cycles), their underlying logic and the consequences of failure are entirely different.
1. Core Comparison
| Feature | [weak self] | [unowned self] |
|---|---|---|
| Captured Type | Becomes an Optional (Controller?) | Remains Non-optional (Controller) |
| Auto-nil-ing | Yes. If self is deallocated, access returns nil. | No. If self is deallocated, the pointer address remains. |
| Crash Risk | Zero Risk. Use guard let for safe handling. | High Risk. Accessing a deallocated self causes a Crash. |
| Overhead | Higher. Requires Side Table access and optional unwrapping. | Lower. Direct memory access similar to strong references. |
| Semantic Assumption | "I'm not sure if you'll still be around when I execute." | "I'm certain you'll still be around when I execute." |
2. Detailed Analysis and Risks
[weak self]: Safe but Verbose
When a closure captures self weakly, it does not increment the reference count.
-
Execution Logic: When the closure runs, it attempts to "promote" the weak reference to a strong one. If
selfhas already been deallocated, the promotion fails and returnsnil. -
Coding Style: Usually requires manual unwrapping.
Swift
service.fetchData { [weak self] in guard let self = self else { return } // Safety guard self.updateUI() } -
Risk: Almost zero risk at the memory level. The only "risk" is logic interruption—if the code inside the closure is critical (e.g., data synchronization) and the object is destroyed, the subsequent logic will simply not run.
[unowned self]: Efficient but Dangerous
unowned is like working at heights without a safety net.
-
Execution Logic: It assumes the object is always alive during the closure's execution. Upon access, the runtime checks the Unowned reference count.
-
The Risk: Runtime Crash. If the closure is an asynchronous callback (e.g., a network request) and the user closes the page (the object
deinits) before the request returns, callingunowned selfwill trigger afatal error.Swift
service.fetchData { [unowned self] in // If self is already destroyed, this CRASHES immediately! self.updateUI() } -
Low-level Detail:
unownedkeeps the object in a "Zombie state"—even thoughdeinithas run, the physical memory is not fully reclaimed until the unowned reference count also reaches zero.
3. Usage Guidelines
When to choose weak self (Recommended for 90% of cases):
- All Asynchronous Callbacks: Network requests, Timers, or complex GCD scheduling.
- Unclear Lifecycles: You aren't sure when the closure will be triggered or if the object will still exist at that time.
When to choose unowned self:
- Lifecycles are Identical: For example, an animation closure inside a View, or a closure that is part of the object's initialization and does not escape.
- Explicit Parent-Child Relationship: For instance, an object holds a closure that will only be called by itself and is guaranteed to stop before the object is destroyed.
4. A Hidden Risk: Premature Exit with [weak self]
Sometimes, using [weak self] causes critical code to simply fail to complete:
- A network request starts;
self(the page) is captured. - The user taps "Back"; the page is destroyed, and the strong count hits zero.
- The network returns; the closure executes;
guard let self = selffails and returns immediately.
If the closure contains logic that must execute (e.g., saving to disk, disconnecting a socket), using weak self might cause these steps to be silently skipped. In such cases, you should re-evaluate your architecture: should this logic live in the object's closure, or should it be moved to a dedicated Data Manager?
Summary Advice
- Beginners / General Business Logic: Use
[weak self]by default. Adding one line ofguard letto ensure app stability is always worth it. - Performance Tuning / Guaranteed Relationships: Use
unownedonly when you are certain the object is alive during execution (e.g., a synchronousforEachclosure, though capture lists are usually unnecessary there).