别让 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("✅ 完美释放!")
}
}
总结:我的避坑座右铭
- 挂起前(await)别解包 :别在网络回来前,用
guard把 VC 锁死。 - 挂起时用可选链 :
self?.logic.request()才是真正的弱引用。 - 回来后再检查:任务结束时,确认 VC 还在,再刷新 UI。
记住:ViewController 是"消耗品",别让它成为你内存里的"传家宝"。
希望这篇文章能帮你理清 Swift 协程的引用谜团!如果你也曾被 deinit 延迟困扰过,欢迎在评论区分享你的经历。👇
下一步建议: 你可以去检查一下项目中那些处理大图上传或复杂计算的 Task,把 guard 后移,你会发现 App 的响应速度和内存表现会有质的飞跃。