8-19.【内存管理机制】如何利用 weak/unowned 打破循环引用?

29 阅读4分钟

在 Swift 中,利用 weakunowned 打破循环引用的核心逻辑是:将原来双向的“强引用环”中的其中一环降级为“非持有引用” 。这样当外部指向该环路的强引用断开时,ARC 就能顺利地将对象销毁。

以下是针对不同场景的具体操作方法:

1. 利用 weak 打破代理模式(Delegate)的循环

这是最常见的场景。通常“父”对象持有“子”对象,而“子”对象通过 delegate 回传消息。

  • 危险代码Table 强引用 DelegateDelegate (通常是 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") }
}

原理ChildParent 的引用不再增加引用计数,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
    }
}

原理CreditCardCustomer 是无主引用,不增加计数。相比 weakunowned 不需要处理 nil 的情况(非可选),代码更简洁且性能略好。


4. 如何选择 weak 还是 unowned

选择适用场景内存行为
weak对象的生命周期可能比引用者短,或者可能变为 nil对象销毁后自动置为 nil最安全
unowned确定被引用的对象在生命周期内永远不会消失(父子关系)。对象销毁后访问会触发 Runtime Crash追求性能和非可选时使用

总结操作步骤:

  1. 识别环路:在 Xcode 中通过 Memory Graph 确认哪两个对象在互相拉扯。

  2. 确定从属:确定谁是“主”(Parent),谁是“从”(Child)。

  3. 降级引用

    • Delegate 设为 weak
    • 闭包捕获 设为 [weak self]
    • 反向关联属性(如子对父)设为 weakunowned

这样做之后,原本的“闭合强引用环”就变成了一条有头有尾的“强引用链”,内存就能正常释放了。

英文版

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: Table strongly references Delegate, and Delegate (usually a VC) strongly references Table.
  • 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 CreditCard and a Customer. 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?

ChoiceApplicable ScenarioMemory Behavior
weakThe 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.
unownedYou 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:

  1. Identify the Loop: Use the Memory Graph Debugger in Xcode to confirm which two objects are pulling on each other.

  2. Determine Ownership: Decide which is the "Owner" (Parent) and which is the "Owned" (Child).

  3. Downgrade the Reference:

    • Set the Delegate to weak.
    • Set Closure Captures to [weak self].
    • Set Reverse Association Properties (e.g., child to parent) to weak or unowned.