求求你,别在 Swift 协程开头写 guard let self = self 了!

别让 guard let self = self 锁死你的控制器:Swift 协程内存"绑架"大揭秘 🚀

01. 一个"灵异"的 deinit

你是否遇到过这种场景:在 ViewController 里的 Task 启动了一个长达 10 秒的图片上传任务。用户觉得太慢,点击"返回"退出了页面。按照 ARC 的逻辑,VC 应该立即销毁。

但控制台却一片寂静。直到 10 秒后上传完成,那个 deinit 才像"诈尸"一样蹦出来。

真相是:你的 ViewController 被"绑架"了,而绑匪正是你亲手写的 guard let self = self


02. 硬核拆解:Task 到底在堆里做了什么?

要理解这个现象,我们得聊聊 Swift 协程的底层存储机制:异步帧(Async Frame)

传统的栈(Stack)

普通函数执行时,变量存在上。函数跑完,栈帧弹出,变量销毁。

协程的堆(Heap)

Task 是为了 await 设计的。await 的本质是 "挂起(Suspension)"

  • 挂起瞬间 :当代码运行到 await,当前线程会被释放去干别的事。为了保证 await 回来后代码能接着跑,Swift 必须把当前函数的所有状态(包括你的强引用 self从栈上拷贝到堆(Heap)上

结论: 只要这个 Task 没运行到最后那个大括号 },堆上的空间就不会销毁。你提前 guard 出来的强引用 self 就像一把锁,把 VC 死死锁在堆里。


03. 别做"暴力解包"的搬运工 🧱

很多开发者习惯在 Task 开头就"暴力解包":

Swift

swift 复制代码
// ❌ 错误示范:VC 将被锁死直到任务结束
Task { [weak self] in
    guard let self = self else { return } // 1. 这里强行"弱转强"
    print("⏳ 上传中...")
    let url = await logic.uploadImage() // 2. 协程挂起,强引用 self 存入堆中
    self.show(url)
}

此时的引用链:

Task (堆内存) ➔ 局部变量 self (强引用) ➔ ViewController

即便用户关了页面,Task 不结束,VC 就释放不了。这就是 "内存延迟释放"


04. 满分范本:后置解包法 💡

我们要做的其实很简单:让任务先跑,人若还在,再干活。

Swift

swift 复制代码
// ✅ 工业级安全模版
Task { [weak self] in
    print("⏳ 任务发起,此时 VC 自由了")
    
    // 关键:不提前 guard,直接 await 发起异步
    let url = await self?.logic.uploadImage() 
    
    // 任务回来了,此时再看"人还在不在"
    guard let self = self else { 
        print("🛑 页面已关,逻辑优雅退出")
        return 
    }
    
    self.show(url) // 只有在这里,才产生瞬时的强引用
}
  • 收益 :用户关掉页面,VC 引用计数清零,立刻执行 deinit

05. 进阶大师:不仅省内存,还要省流量 📡

既然观众离场了,戏台确实不该演了。对于耗时极长的任务,我们应该在 deinit 时主动取消。

Swift

swift 复制代码
class MasterVC: UIViewController {
    private var loadTask: Task<Void, Never>?

    func start() {
        loadTask?.cancel()
        loadTask = Task { [weak self] in
            let res = await logic.longTimeRequest()
            guard let self = self, !Task.isCancelled else { return }
            self.done(res)
        }
    }

    deinit {
        loadTask?.cancel() // 页面关,网络停,流量省
        print("✅ 完美释放!")
    }
}

总结:我的避坑座右铭

  1. 挂起前(await)别解包 :别在网络回来前,用 guard 把 VC 锁死。
  2. 挂起时用可选链self?.logic.request() 才是真正的弱引用。
  3. 回来后再检查:任务结束时,确认 VC 还在,再刷新 UI。

记住:ViewController 是"消耗品",别让它成为你内存里的"传家宝"。


希望这篇文章能帮你理清 Swift 协程的引用谜团!如果你也曾被 deinit 延迟困扰过,欢迎在评论区分享你的经历。👇

下一步建议: 你可以去检查一下项目中那些处理大图上传或复杂计算的 Task,把 guard 后移,你会发现 App 的响应速度和内存表现会有质的飞跃。

相关推荐
摸鱼的春哥22 分钟前
Agent教程15:认识LangChain(中),状态机思维
前端·javascript·后端
明月_清风27 分钟前
告别遮挡:用 scroll-padding 实现优雅的锚点跳转
前端·javascript
明月_清风30 分钟前
原生 JS 侧边栏缩放:从 DOM 监听到底层优化
前端·javascript
万少9 小时前
HarmonyOS 开发必会 5 种 Builder 详解
前端·harmonyos
橙序员小站12 小时前
Agent Skill 是什么?一文讲透 Agent Skill 的设计与实现
前端·后端
炫饭第一名14 小时前
速通Canvas指北🦮——基础入门篇
前端·javascript·程序员
王晓枫14 小时前
flutter接入三方库运行报错:Error running pod install
前端·flutter
符方昊14 小时前
React 19 对比 React 16 新特性解析
前端·react.js
ssshooter15 小时前
又被 Safari 差异坑了:textContent 拿到的值居然没换行?
前端
曲折15 小时前
Cesium-气象要素PNG色斑图叠加
前端·cesium