题目
Swift中如何安全处理闭包与类实例之间的循环引用?
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
ARC原理,闭包捕获语义,weak/unowned选择,内存泄漏检测
快速回答
在Swift中处理闭包与类实例的循环引用需要:
- 理解ARC对引用计数的管理机制
- 使用捕获列表(capture list)声明弱引用或非持有引用
- 根据对象生命周期选择
weak或unowned - 避免在闭包内隐式捕获
self
示例解决方案:{ [weak self] in
guard let self = self else { return }
// 操作self
}
解析
1. 问题本质与原理
Swift使用自动引用计数(ARC)管理内存。当闭包捕获类实例时:
- 闭包是引用类型,会强引用捕获的对象
- 若类实例也持有该闭包,则形成循环引用
- 导致对象无法释放,引发内存泄漏
class DataProcessor {
var onComplete: (() -> Void)?
func process() {
// 闭包隐式捕获self(强引用)
onComplete = {
self.finish() // ❌ 循环引用
}
}
deinit { print("释放") }
}
let processor = DataProcessor()
processor.process() // processor和onComplete相互持有
2. 解决方案:捕获列表
使用捕获列表显式指定捕获方式:
// 正确写法
onComplete = { [weak self] in
guard let self = self else { return }
self.finish()
}
// 或(仅当self生命周期不短于闭包时)
onComplete = { [unowned self] in
self.finish()
}
3. weak vs unowned 选择原则
| weak | unowned |
|---|---|
| 捕获对象可能为nil时使用 | 对象与闭包生命周期相同或更长时使用 |
| 返回可选类型,需解包 | 非可选,但对象释放后访问会崩溃 |
| 安全但稍繁琐 | 高效但有风险 |
4. 最佳实践
- 默认使用weak:除非能100%确定对象存活
- 避免隐式捕获:始终显式声明捕获列表
- 链式调用注意:
[weak parent] in parent?.child?.doSomething() - 检测工具:Xcode Memory Graph Debugger / Instruments Leaks
5. 复杂场景处理
多对象捕获:
service.fetchData { [weak viewController, weak dataModel] result in
guard let vc = viewController, let model = dataModel else { return }
vc.updateUI(with: result)
model.cache(result)
}
异步竞争:
class ImageLoader {
private var task: URLSessionTask?
func load(url: URL, completion: @escaping (UIImage?) -> Void) {
task = URLSession.shared.dataTask(with: url) { [weak self] data, _, _ in
// 后台线程执行,需判断self有效性
guard self != nil else { return }
// 处理图片...
DispatchQueue.main.async {
completion(image)
}
}
task?.resume()
}
func cancel() { task?.cancel() }
}
6. 常见错误
- 忘记在闭包内解包weak self(直接使用self报错)
- 误用unowned导致EXC_BAD_ACCESS崩溃
- 在闭包内调用会延长self生命周期的方法(如DispatchQueue.main.async)
- 未及时取消网络请求/定时器等后台任务
7. 扩展知识
- 闭包值捕获:值类型在闭包内被复制,但引用类型仍按引用捕获
- @escaping闭包:异步操作必须显式声明,编译器会强制捕获列表检查
- weak-strong dance:
guard let strongSelf = self else { return }避免操作过程中对象释放 - 协议解决方案:使用
AnyObject约束协议,结合weak var delegate