循环引用(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. 异步操作中的闭包(网络请求、定时器等)
如果闭包被异步任务(如 URLSession
、DispatchQueue
、Timer
)长期持有,且闭包中强引用了 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,因为闭包在动画开始后立即释放
}
原因:
- 非逃逸闭包的生命周期仅限于函数执行期间,执行完毕后自动释放。
如何检测循环引用?
- 观察
deinit
是否被调用:如果对象未释放,则可能发生了循环引用。 - 使用 Instruments 的
Leaks
工具:检测内存泄漏。 - Xcode 内存图调试器:查看对象的引用关系。
总结:循环引用的触发条件
- 对象之间相互强引用(如父子对象)。
- 闭包强引用
self
,且闭包被长期持有(如存储为属性、异步任务回调)。 - 隐式捕获
self
(即使没有显式写self
,但访问了实例属性或方法)。
在 Swift 中,闭包是否导致循环引用的核心在于:闭包是否被长期持有。如果闭包是临时使用并立即释放(如 SnapKit 的 updateConstraints
),则无需担心;如果闭包被存储或异步持有,则必须处理弱引用。