如何按顺序执行带Completion Handler的函数?Dispatch Queues使用遇阻
我太懂你这个困扰了——本来想用Dispatch Queues让三个带Completion Handler的函数老老实实按顺序跑,前一个彻底干完再启动下一个,结果却是“调用顺序对了,但前一个还在干活呢,下一个就已经跑起来了”。这大概率是因为你没把Completion Handler的回调逻辑和队列的等待机制结合起来,毕竟像图片下载这种操作本身就是异步的,直接丢队列里只会发起调用,不会等它真正完成。
问题根源拆解
先给你举个典型的错误写法,是不是你之前大概这么写的?
let serialQueue = DispatchQueue(label: "com.your.custom.queue") serialQueue.async { self.downloadImage { _ in } } serialQueue.async { self.secondFunction { _ in } } serialQueue.async { self.thirdFunction { _ in } }
这时候串行队列确实会按顺序发起三个函数的调用,但downloadImage内部是异步操作(比如URLSession的网络请求),队列里的第一个任务“发起下载”其实几毫秒就结束了,立刻就跑去执行第二个函数的调用,根本没等下载完成的Completion Handler触发。
正确的实现方式
核心思路就是:把下一个函数的启动逻辑,放到前一个函数的Completion Handler里面,同时用队列来控制这些回调的执行线程,避免线程混乱。这里给你两种常用的靠谱方案:
方案1:串行队列 + Completion Handler链式调用
这是最直接的写法,把每个后续任务的启动逻辑嵌在前一个的Completion里,用串行队列保证所有回调都在同一个线程执行:
let serialQueue = DispatchQueue(label: "com.your.serial.task.queue") // 启动第一个任务 serialQueue.async { self.downloadImage { [weak self] downloadedImage in // 第一个任务彻底完成后,在同一个队列启动第二个 guard let self = self else { return } serialQueue.async { self.secondFunction { secondResult in // 第二个完成后启动第三个 serialQueue.async { self.thirdFunction { finalResult in // 所有任务全部完成 print("All tasks finished successfully!") } } } } } }
如果你的函数Completion Handler默认在主线程回调(比如URLSession的默认回调),记得把后续任务调度到串行队列,别让主线程做太多耗时操作。
方案2:用DispatchGroup优化链式结构(更优雅)
要是你不想写嵌套的Completion Handler,可以用DispatchGroup来跟踪每个异步任务的完成状态,代码可读性会好很多:
let serialQueue = DispatchQueue(label: "com.your.group.task.queue") let taskGroup = DispatchGroup() serialQueue.async { taskGroup.enter() self.downloadImage { [weak self] image in defer { taskGroup.leave() } guard let self = self else { return } // 第一个任务完成后,通知group启动第二个 taskGroup.notify(queue: serialQueue) { taskGroup.enter() self.secondFunction { result in defer { taskGroup.leave() } // 第二个完成后启动第三个 taskGroup.notify(queue: serialQueue) { self.thirdFunction { finalResult in print("All tasks done!") } } } } } }
这种方式把嵌套逻辑拆解开,后续要加更多任务也更容易维护。
关键注意点
- 绝对要在Completion Handler内部触发下一个任务,而不是直接把函数调用丢队列里。
- 闭包嵌套时一定要用
[weak self],避免循环引用把内存搞炸。 - 如果涉及UI更新,记得在Completion Handler里切回主线程,比如:
self.downloadImage { image in DispatchQueue.main.async { self.imageView.image = image } // 继续下一个任务... }
内容的提问来源于stack exchange,提问作者Dav Irv

