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

相关推荐
1024小神13 小时前
xcode 中配置AR Resource Group并设置图片宽度等
ios·swiftui·ar·xcode·swift
Wcowin1 天前
OneClip 开发经验分享:从零到一的 macOS 剪切板应用开发
mac·swift·粘贴板
如此风景1 天前
iOS SwiftUI开发所有修饰符使用详解
ios
mumuWorld1 天前
KSCrash 实现机制深度分析
ios·源码阅读
AskHarries1 天前
Google 登录问题排查指南
flutter·ios·app
崽崽长肉肉1 天前
Swift中的知识点总结
ios·swift
2501_916007471 天前
苹果手机iOS应用管理全指南与隐藏功能详解
android·ios·智能手机·小程序·uni-app·iphone·webview
代码不行的搬运工1 天前
面向RDMA网络的Swift协议
开发语言·网络·swift
2501_915106321 天前
全面理解 iOS 帧率,构建从渲染到系统行为的多工具协同流畅度分析体系
android·ios·小程序·https·uni-app·iphone·webview
TouchWorld1 天前
iOS逆向-哔哩哔哩增加3倍速(1)-最大播放速度
ios·逆向