求求你,别在 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 的响应速度和内存表现会有质的飞跃。

相关推荐
Justin3go16 小时前
HUNT0 上线了——尽早发布,尽早发现
前端·后端·程序员
怕浪猫16 小时前
第一章 JSX 增强特性与函数组件入门
前端·javascript·react.js
铅笔侠_小龙虾16 小时前
Emmet 常用用法指南
前端·vue
钦拆大仁17 小时前
跨站脚本攻击XSS
前端·xss
VX:Fegn089518 小时前
计算机毕业设计|基于springboot + vue校园社团管理系统(源码+数据库+文档)
前端·数据库·vue.js·spring boot·后端·课程设计
ChangYan.19 小时前
直接下载源码但是执行npm run compile后报错
前端·npm·node.js
skywalk816319 小时前
在 FreeBSD 上可以使用的虚拟主机(Web‑Hosting)面板
前端·主机·webmin
ohyeah20 小时前
深入理解 React 中的 useRef:不只是获取 DOM 元素
前端·react.js
MoXinXueWEB20 小时前
前端页面获取不到url上参数值
前端
低保和光头哪个先来20 小时前
场景6:对浏览器内核的理解
开发语言·前端·javascript·vue.js·前端框架