循环引用场景

11 阅读3分钟

循环引用(Retain Cycle)通常发生在两个或多个对象相互强引用导致无法释放时,或者在对象与闭包之间形成强引用环的场景中。以下是常见会发生循环引用的场景及具体示例:


1. 对象之间的相互强引用

当两个对象彼此持有对方的强引用时,就会形成循环引用,导致两者都无法释放。

示例:双向强引用的父子对象

swift

class Parent {
    var child: Child?
}

class Child {
    var parent: Parent?  // 强引用 Parent 对象
}

let parent = Parent()
let child = Child()
parent.child = child
child.parent = parent  // Parent ↔ Child 相互强引用 → 循环引用

解决方法:

  • 将其中一方的引用改为 weak 或 unowned

swift

class Child {
    weak var parent: Parent?  // 弱引用打破循环
}

2. 闭包捕获 self 且闭包被长期持有

当闭包隐式或显式地捕获 self(强引用),同时闭包又被 self 的属性长期持有,就会形成循环引用。

示例:闭包作为对象的属性存储

swift

class MyViewController: UIViewController {
    var completionHandler: (() -> Void)?  // 闭包被对象强引用
    
    func setup() {
        completionHandler = { 
            self.doSomething()  // 闭包隐式强引用 self → 循环引用
        }
    }
    
    func doSomething() { ... }
}

解决方法:

  • 使用 [weak self] 或 [unowned self] 弱捕获 self

swift

completionHandler = { [weak self] in
    self?.doSomething()
}

3. 异步操作中的闭包(网络请求、定时器等)

如果闭包被异步任务(如 URLSessionDispatchQueueTimer)长期持有,且闭包中强引用了 self,就会导致循环引用。

示例:网络请求的回调闭包

swift

class DataLoader {
    func fetchData(completion: @escaping (Data) -> Void) {
        URLSession.shared.dataTask(with: url) { data, _, _ in
            completion(data)  // 如果 completion 强引用 self → 循环引用
        }.resume()
    }
}

class ViewController: UIViewController {
    let loader = DataLoader()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        loader.fetchData { [weak self] data in  // 必须 weak self
            self?.handleData(data)
        }
    }
}

关键点:

  • 如果 fetchData 的 completion 闭包被强引用(例如被 URLSession 任务持有),而闭包内部又强引用了 self,则会导致循环引用。

4. 闭包间接持有外部对象

即使闭包没有直接使用 self,但通过访问对象的属性或方法隐式捕获 self,也可能导致循环引用。

示例:闭包隐式捕获 self

swift

class MyClass {
    var count = 0
    var closure: (() -> Void)?
    
    func setup() {
        closure = {
            self.count += 1  // 隐式捕获 self → 闭包强引用 self
        }
    }
}

let obj = MyClass()
obj.setup()  // obj 强引用 closure,closure 强引用 obj → 循环引用

解决方法:

  • 显式声明 [weak self]

swift

closure = { [weak self] in
    self?.count += 1
}

5. 非逃逸闭包(No Escape Closures)不会导致循环引用

如果闭包是非逃逸的(例如作为函数参数,且在函数返回前执行完毕),则不会导致循环引用,因为闭包不会被外部持有。

示例:UIView.animate 的闭包(非逃逸)

swift

UIView.animate(withDuration: 0.5) {
    self.view.alpha = 0  // 无需 weak self,因为闭包在动画开始后立即释放
}

原因:

  • 非逃逸闭包的生命周期仅限于函数执行期间,执行完毕后自动释放。

如何检测循环引用?

  1. 观察 deinit 是否被调用:如果对象未释放,则可能发生了循环引用。
  2. 使用 Instruments 的 Leaks 工具:检测内存泄漏。
  3. Xcode 内存图调试器:查看对象的引用关系。

总结:循环引用的触发条件

  1. 对象之间相互强引用(如父子对象)。
  2. 闭包强引用 self,且闭包被长期持有(如存储为属性、异步任务回调)。
  3. 隐式捕获 self(即使没有显式写 self,但访问了实例属性或方法)。

在 Swift 中,闭包是否导致循环引用的核心在于:闭包是否被长期持有。如果闭包是临时使用并立即释放(如 SnapKit 的 updateConstraints),则无需担心;如果闭包被存储或异步持有,则必须处理弱引用。