Swift 并发任务中到底该不该用 `[weak self]`?—— 从原理到实战一次讲透

为什么闭包里总写 [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)
        }
    }
}

同时:

  1. @escaping 闭包不会延长对象生命周期,一般不需要 [weak self]
  2. 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 一直被强引用
    }
}

真正想"随用随放"的正确姿势

  1. 只在真正需要时临时强引用
swift 复制代码
// ✅ 推荐:延迟获取强引用
Task { [weak self] in
    let data = await loadData()
    guard let self else { return }   // 真正需要时才升级
    let models = await self.processData(data)
    // 用完后立即释放
}
  1. 循环体里每次迭代都检查
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
        }
    }
}
  1. 干脆不引用整个 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]) { ... }
}

要点拆解:

  1. Task 捕获 [weak self],但不在第一行就 guard let self
  2. while 内每次迭代前再检查,确保对象销毁时能及时退出。
  3. 更新 UI 或清理 task 时再次使用 [weak self],避免闭包内部重新产生强引用。

五、一句话总结 + 思维导图

场景 是否需要 [weak self] 关键口诀
@escaping 闭包 不逃逸,不延长寿命
普通短时 Task 可选 想更早释放就延迟 guard
长时/循环 Task 循环内部每次 guard
只依赖个别属性 捕获具体成员,而非整个 self

Structured Concurrency 是不是就高枕无忧?

async-letTaskGroupwithTaskGroup 这类结构化并发会在控制流离开作用域时自动等待或取消任务,看起来"不会泄漏"。

但如果任务内部又起了一个未捕获的异步线程(如 Task.detached 或后台全局队列),仍然可能强引用 self。

因此:

  1. 能结构化就结构化,少写裸 Task
  2. 必须裸 Task 时,就按本文方案处理 [weak self]
  3. 善用 withTaskCancellationHandler 及时清理外部资源。

实践经验

  1. 把"延迟 guard let self"写进团队 Code Review 清单,第一行就 guard 直接打回。
  2. 对生命周期长的任务,一律在循环/关键节点重新检查 self;日志里加上 os_signpost,方便 Instruments 追踪。
  3. 如果 VM / Manager 层只是做数据加工,不要直接传 self 给 Repository,而是把所需参数、回调、Continuation 包装成值类型,再交由后台任务处理,彻底断掉引用链。

结语

[weak self] 不是"写了就安全",更不是"一律不用写"。

在 Swift 并发时代,任务的生命周期与对象生命周期往往不同步,只有"在需要时再去强引用、用完立刻放"才是内存安全的真谛。

相关推荐
FeliksLv3 小时前
iOS 集成mars xlog
ios
2501_915106325 小时前
CDN 可以实现 HTTPS 吗?实战要点、部署模式与真机验证流程
网络协议·http·ios·小程序·https·uni-app·iphone
大熊猫侯佩7 小时前
天网代码反击战:Swift 三元运算符的 “一行破局” 指南
swiftui·swift·apple
大熊猫侯佩1 天前
在肖申克监狱玩转 iOS 26:安迪的 Liquid Glass 复仇计划
ios·swiftui·swift
大熊猫侯佩1 天前
用最简单的方式让 SwiftUI 画一颗爱你的小红心
swiftui·swift·apple
HarderCoder1 天前
Swift 初探:从变量到并发,一文带你零基础读懂官方 Tour
swift
非专业程序员2 天前
逆向分析CoreText中的字体级联/Font Fallback机制
前端·ios