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

相关推荐
清妍_1 小时前
一文详解 Taro / 小程序 IntersectionObserver 参数
前端
电商API大数据接口开发Cris1 小时前
构建异步任务队列:高效批量化获取淘宝关键词搜索结果的实践
前端·数据挖掘·api
符方昊1 小时前
如何实现一个MCP服务器
前端
喝咖啡的女孩1 小时前
React useState 解读
前端
渴望成为python大神的前端小菜鸟1 小时前
浏览器及其他 面试题
前端·javascript·ajax·面试题·浏览器
1024肥宅2 小时前
手写 new 操作符和 instanceof:深入理解 JavaScript 对象创建与原型链检测
前端·javascript·ecmascript 6
吃肉的小飞猪2 小时前
uniapp 下拉刷新终极方案
前端
关关长语2 小时前
Vue本地部署包快速构建为Docker镜像
前端·vue.js·docker
jump6802 小时前
react的事件优先级
前端