在 Swift 中,利用 weak 和 unowned 打破循环引用的核心逻辑是:将原来双向的“强引用环”中的其中一环降级为“非持有引用” 。这样当外部指向该环路的强引用断开时,ARC 就能顺利地将对象销毁。
以下是针对不同场景的具体操作方法:
1. 利用 weak 打破代理模式(Delegate)的循环
这是最常见的场景。通常“父”对象持有“子”对象,而“子”对象通过 delegate 回传消息。
- 危险代码:
Table强引用Delegate,Delegate(通常是 VC) 又强引用Table。 - 解决方案:将代理属性声明为
weak。
Swift
protocol MyDelegate: AnyObject { // 1. 协议必须继承自 AnyObject
func didUpdate()
}
class Child {
weak var delegate: MyDelegate? // 2. 使用 weak 修饰,且必须是 Optional
}
class Parent: MyDelegate {
var child = Child()
init() { child.delegate = self }
func didUpdate() { print("Updated") }
}
原理:Child 对 Parent 的引用不再增加引用计数,Parent 销毁时,delegate 自动变为 nil。
2. 利用捕获列表(Capture List)打破闭包循环
当对象持有一个闭包,且闭包内部又使用了 self 时。
- 危险代码:
self.callback = { self.doSomething() } - 解决方案:在闭包起始位置添加
[weak self]或[unowned self]。
Swift
class MyController {
var onComplete: (() -> Void)?
func setup() {
// 使用捕获列表打破循环
onComplete = { [weak self] in
// 这里的 self 变成了 Optional
guard let self = self else { return }
self.doSomething()
}
}
func doSomething() { print("Done") }
}
原理:闭包内部存储的是 self 的弱引用,不会导致 MyController 无法释放。
3. 利用 unowned 打破父子双向引用
当两个对象的生命周期高度绑定,且其中一个对象(子)绝不会比另一个对象(父)活得更久时,使用 unowned。
- 场景:信用卡(CreditCard)和持卡人(Customer)。没有持卡人,信用卡就不该存在。
- 代码实现:
Swift
class Customer {
var card: CreditCard?
}
class CreditCard {
unowned let customer: Customer // 非可选,性能比 weak 高
init(customer: Customer) {
self.customer = customer
}
}
原理:CreditCard 对 Customer 是无主引用,不增加计数。相比 weak,unowned 不需要处理 nil 的情况(非可选),代码更简洁且性能略好。
4. 如何选择 weak 还是 unowned?
| 选择 | 适用场景 | 内存行为 |
|---|---|---|
weak | 对象的生命周期可能比引用者短,或者可能变为 nil。 | 对象销毁后自动置为 nil。最安全。 |
unowned | 确定被引用的对象在生命周期内永远不会消失(父子关系)。 | 对象销毁后访问会触发 Runtime Crash。追求性能和非可选时使用。 |
总结操作步骤:
-
识别环路:在 Xcode 中通过 Memory Graph 确认哪两个对象在互相拉扯。
-
确定从属:确定谁是“主”(Parent),谁是“从”(Child)。
-
降级引用:
- 将 Delegate 设为
weak。 - 将 闭包捕获 设为
[weak self]。 - 将 反向关联属性(如子对父)设为
weak或unowned。
- 将 Delegate 设为
这样做之后,原本的“闭合强引用环”就变成了一条有头有尾的“强引用链”,内存就能正常释放了。
英文版
8-19. [Memory Management] How to use weak and unowned to break strong reference cycles?
In Swift, the core logic of using weak and unowned to break a strong reference cycle is to downgrade one link in the bi-directional "strong reference ring" to a "non-owning reference." This allows ARC to successfully deallocate the objects when the external strong references to that ring are broken.
Here are the specific operation methods for different scenarios:
1. Breaking Delegate Cycles with weak
This is the most common scenario. Usually, a "parent" object holds a "child" object, and the "child" sends messages back via a delegate.
- Dangerous Code:
Tablestrongly referencesDelegate, andDelegate(usually a VC) strongly referencesTable. - Solution: Declare the delegate property as
weak.
Swift
protocol MyDelegate: AnyObject { // 1. Protocol must inherit from AnyObject
func didUpdate()
}
class Child {
weak var delegate: MyDelegate? // 2. Use weak modifier; must be Optional
}
class Parent: MyDelegate {
var child = Child()
init() { child.delegate = self }
func didUpdate() { print("Updated") }
}
Principle: The reference from Child to Parent no longer increments the reference count. When Parent is deallocated, the delegate property automatically becomes nil.
2. Breaking Closure Cycles with Capture Lists
This occurs when an object holds a closure, and that closure uses self internally.
- Dangerous Code:
self.callback = { self.doSomething() } - Solution: Add
[weak self]or[unowned self]at the beginning of the closure.
Swift
class MyController {
var onComplete: (() -> Void)?
func setup() {
// Use a capture list to break the cycle
onComplete = { [weak self] in
// self becomes Optional here
guard let self = self else { return }
self.doSomething()
}
}
func doSomething() { print("Done") }
}
Principle: The closure stores a weak reference to self internally, ensuring it doesn't prevent MyController from being released.
3. Breaking Parent-Child Cycles with unowned
Use unowned when two objects have highly interdependent lifecycles, and one object (the child) is guaranteed never to outlive the other (the parent).
- Scenario: A
CreditCardand aCustomer. A credit card shouldn't exist without a cardholder. - Implementation:
Swift
class Customer {
var card: CreditCard?
}
class CreditCard {
unowned let customer: Customer // Non-optional, better performance than weak
init(customer: Customer) {
self.customer = customer
}
}
Principle: CreditCard has an unowned reference to Customer, which does not increment the count. Compared to weak, unowned does not require handling nil (it is non-optional), making the code cleaner and slightly more performant.
4. How to choose between weak and unowned?
| Choice | Applicable Scenario | Memory Behavior |
|---|---|---|
weak | The object's lifecycle might be shorter than the referrer, or it might become nil. | Automatically set to nil after the object is destroyed. Safest option. |
unowned | You are certain the referenced object will never disappear during the referrer's lifecycle (Parent-Child). | Accessing after the object is destroyed triggers a Runtime Crash. Use for performance and non-optionality. |
Summary of Operation Steps:
-
Identify the Loop: Use the Memory Graph Debugger in Xcode to confirm which two objects are pulling on each other.
-
Determine Ownership: Decide which is the "Owner" (Parent) and which is the "Owned" (Child).
-
Downgrade the Reference:
- Set the Delegate to
weak. - Set Closure Captures to
[weak self]. - Set Reverse Association Properties (e.g., child to parent) to
weakorunowned.
- Set the Delegate to