为什么闭包里总写 [weak self]
成了肌肉记忆?
在 Swift 回调式 API 时代,我们被教育"只要闭包可能产生循环引用,就写 [weak self]
"。
这个经验在 @escaping
闭包里确实有效:
swift
// 传统回调,容易产生循环引用
class ListVC {
var loadData: (@escaping ([Model]) -> Void) -> Void = { _ in }
func viewDidLoad() {
// 弱引用避免循环引用
loadData { [weak self] models in
guard let self = self else { return }
self.reloadUI(with: models)
}
}
}
同时:
- 非
@escaping
闭包不会延长对象生命周期,一般不需要[weak self]
。 - SE-0269 允许"隐式 self"后,我们又少了写
self.
的机会,更容易忘记引用关系。
Task {} 是不是"闭包"?要不要也写 [weak self]
?
Task { ... }
是一个逃逸闭包,但它会立即被调度执行,并且生命周期不依赖创建者。
因此,第一行 guard let self
就会立即把弱引用变成强引用,等于"白写了":
swift
// ❌ 这样写其实没解决泄漏
func loadModels() {
Task { [weak self] in // 1. 弱引用
guard let self else { return } // 2. 瞬间变强引用
let data = await loadData()
let models = await processData(data)
} // 3. 整个异步过程 self 一直被强引用
}
对比回调时代,等价于:
swift
// 等价于回调里根本没写 [weak self]
loadData { data in
self.processData(data) { models in
// self 一直被强引用
}
}
真正想"随用随放"的正确姿势
- 只在真正需要时临时强引用
swift
// ✅ 推荐:延迟获取强引用
Task { [weak self] in
let data = await loadData()
guard let self else { return } // 真正需要时才升级
let models = await self.processData(data)
// 用完后立即释放
}
- 循环体里每次迭代都检查
swift
// ✅ 长任务场景:每页拉取
func loadAllPages() {
fetchPagesTask = Task { [weak self] in
var hasMore = true
while hasMore && !Task.isCancelled {
guard let self else { break } // 每轮重新检查
let page = await self.fetchNextPage()
hasMore = !page.isLast
}
}
}
- 干脆不引用整个
self
,只捕获所需成员
swift
// ✅ 极致解耦:只拿需要的数据与函数
Task { [weak cache, weak fetcher] in
let data = await fetcher?.next()
cache?.store(data)
}
完整实战:把"弱引用"写进业务层
假设我们有一个图片瀑布流 VM,需要持续拉取分页:
swift
final class WaterfallVM {
private var currentPage = 0
private var task: Task<Void, Never>?
deinit {
task?.cancel()
}
/// 开始拉取,重复调用不会重复启动
func startIfNeeded() {
guard task == nil else { return }
task = Task { [weak self] in
while !Task.isCancelled {
guard let self else { break } // 每轮重新检查
let page = await self.fetch(page: self.currentPage)
// 回到主线程更新 UI
await MainActor.run {
self.append(page.items)
}
guard !page.isLast else { break }
self.currentPage += 1
// 每轮结束都重新检查 self 是否存在
// 如果 VC 被 pop,下一轮会自动 break
}
// 清理 task 引用,允许下次重新启动
await MainActor.run { [weak self] in
self?.task = nil
}
}
}
private func fetch(page: Int) async -> Page<Item> { ... }
private func append(_ items: [Item]) { ... }
}
要点拆解:
Task
捕获[weak self]
,但不在第一行就guard let self
。- 在
while
内每次迭代前再检查,确保对象销毁时能及时退出。 - 更新 UI 或清理
task
时再次使用[weak self]
,避免闭包内部重新产生强引用。
五、一句话总结 + 思维导图
场景 | 是否需要 [weak self] |
关键口诀 |
---|---|---|
非 @escaping 闭包 |
❌ | 不逃逸,不延长寿命 |
普通短时 Task | 可选 | 想更早释放就延迟 guard |
长时/循环 Task | ✅ | 循环内部每次 guard |
只依赖个别属性 | ✅ | 捕获具体成员,而非整个 self |
Structured Concurrency 是不是就高枕无忧?
async-let
、TaskGroup
、withTaskGroup
这类结构化并发会在控制流离开作用域时自动等待或取消任务,看起来"不会泄漏"。
但如果任务内部又起了一个未捕获的异步线程(如 Task.detached
或后台全局队列),仍然可能强引用 self。
因此:
- 能结构化就结构化,少写裸
Task
。 - 必须裸
Task
时,就按本文方案处理[weak self]
。 - 善用
withTaskCancellationHandler
及时清理外部资源。
实践经验
- 把"延迟
guard let self
"写进团队 Code Review 清单,第一行就guard
直接打回。 - 对生命周期长的任务,一律在循环/关键节点重新检查
self
;日志里加上os_signpost
,方便 Instruments 追踪。 - 如果 VM / Manager 层只是做数据加工,不要直接传
self
给 Repository,而是把所需参数、回调、Continuation 包装成值类型,再交由后台任务处理,彻底断掉引用链。
结语
[weak self]
不是"写了就安全",更不是"一律不用写"。
在 Swift 并发时代,任务的生命周期与对象生命周期往往不同步,只有"在需要时再去强引用、用完立刻放"才是内存安全的真谛。