我为什么从 async/await 逃回了回调?

🔑 关键词:Swift并发, async/await, 回调地狱, iOS开发, 任务取消

📖 摘要:当所有人都吹捧 async/await 的时候,我在真实项目中吃尽了苦头。这篇不是教程,是反方向的踩坑记录——到底什么场景下回调反而更合理?

我为什么从 async/await 逃回了回调?

图片

先声明,我曾经也是 async/await 的吹鼓手。WWDC 2021 那场视频刷了三遍,看到结构化并发时热血上头。记得当时把项目里最后的 UICollectionView 数据加载改成 async/await,心里美滋滋,以为从此告别嵌套地狱。但真正跑了三个月,我发现了一个没人提的真相:回调的本质是显式、可预测的,而 async/await 把控制流变成了一张蜘蛛网。

图片

举一个很具体的例子:在音视频剪辑项目里,需要先读取素材元数据,然后缩略图生成,再混合音轨。用 async/await 写的话,开始确实像这样几句顺序代码。可一旦崩溃,你能在堆栈里看到的只有 Coroutine 框架的跳转地址,哪一层被中断、被取消,全是黑盒。反而用调回调,虽然丑得像千层饼,但每层都是真实的内存地址,调试器里一目了然。别跟我提 Instruments 的时间线,崩溃现场就是另一回事。

图片

另一个坑在网络层。我们用 async/await 包装了 URLSession,发现下游想拿到本次请求的准确尺寸、错误重次数变得很吃力。Task 的取消机制像极了没主见的传话筒——你给一个小请求挂后台,父任务一取消,它跟着不见了,连中间状态都不留。之前用 OperationQueue 加 completionBlock 时,能让正在执行的网络任务优雅终止,顺手记录到埋点里。现在倒好,取消完最后一片落叶都找不到。苹果工程师会说任务树是符合预期的,可现实业务里的“后台同步完再上传缓存”根本没法匹配那种树形结构,它就是一张有向图。

图片

当然,我并不是要倒退回远古时代的嵌套地狱。如果你做的是 UI 点击后的一次性异步,那 async/await 简直完美。但只要是牵扯到上传重试、轮询、断点续传这类有状态操作,建议你把部分链路改回回调或者 Combine。我在自己的小库就做了混合方案:网络层保留 URLSession 的 dataTask,业务层用 async 包装一层薄薄的壳。别追求代码长得像散文,为了每行顺序可递归,我宁愿用闭包手动传 state。

图片

最后想吐槽的是团队协作层面的度量——每个实习生写出来的 async/await 风格都不一样。有的人习惯 withCheckedThrowingContinuation 硬桥接,有人干脆傻傻地 await 一个永不发信号的 Actor。代码 review 变成宗教审判:什么人觉得必须全程 async,什么人觉得能不要就尽量 return。回调时代约定俗成的 [weak self] 检查,到了结构化并发反而滋生了一份从容的傲慢——总感觉有运行时兜底。事实是,我删掉了半年内核异步代码,反而稳了。如果这篇文章能让几个人停下来思考“我是不是为了并发而并发”,那就算值了。

图片

🏷️ 标签: