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

相关推荐
RobinDevNotes10 分钟前
从 Electron 到 GPUI,9 个跨平台桌面方案该怎么选
前端·javascript·rust·electron·openresty·前端开发工具
jay神1 小时前
基于 YOLOv8 的轴承缺陷检测(Web 版)
前端·人工智能·yolo
jay神1 小时前
基于 YOLOv8 的森林火灾检测系统(Web 版)
前端·yolo
code2cat1 小时前
HTML基础篇之第一张网页:从index.html到浏览器里的任务清单
前端·html
miss2 小时前
AI 改完不敢上线?OpenSpec 规约驱动开发实战
前端·代码规范·前端工程化
AiMan3 小时前
腾讯ima还是不可以导出左侧所有对话,只能单条导出,方法不对吗
前端
去伪存真3 小时前
并发量一高就装死?Agent 全链路“防猝死”手册
前端·agent
Moment3 小时前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
AiMan3 小时前
先确认能不能一次把Microsofat Copilot所有内容打包一个文件导出
前端
总要学点什么3 小时前
第97篇:三维数据加载-城市建筑白模自定义着色器——让一条光带沿高度扫上去,快慢却由帧率说了算
前端·gis·webgl·cesium·着色器·空间分析·白模