Swift循环中异步调用问题:如何实现串行执行异步任务
解决异步循环顺序处理评论回复的问题
看起来你遇到了异步编程里的经典坑:你的for循环是同步执行的,会立刻发起所有fetchDetailsAboutCommentReply请求,但这些请求的回调是异步触发的——等回调跑起来的时候,循环已经跑完了,i的值早就变成数组最后一个索引了,这就导致你插入dataSource的索引全错,还会触发错误的完成判断,最终崩溃。
下面给你几种可行的解决方案,从兼容旧iOS版本到现代异步写法都有:
方案1:递归顺序处理(兼容所有iOS版本)
用递归代替for循环,处理完一个评论回复的详情后,再处理下一个,确保顺序执行:
首先定义一个递归处理函数:
private func processReplies(_ replies: [CommentReply], currentIndex: Int, baseInsertIndex: Int, dataSource: inout [CommentItem], indexPaths: inout [IndexPath], completion: @escaping () -> Void) { // 所有回复处理完成,回调代理 guard currentIndex < replies.count else { self.delegate?.didLoadReplies(dataSource: dataSource, ips: indexPaths) completion() return } let currentReply = replies[currentIndex] self.fetchDetailsAboutCommentReply(commentReplyObject: currentReply) { commentItem in commentItem.commentObject = currentReply // 计算正确的插入索引 let insertIndex = baseInsertIndex + currentIndex + 1 dataSource.insert(commentItem, at: insertIndex) indexPaths.append(IndexPath(row: insertIndex, section: 0)) // 递归处理下一个回复 self.processReplies(replies, currentIndex: currentIndex + 1, baseInsertIndex: baseInsertIndex, dataSource: &dataSource, indexPaths: &indexPaths, completion: completion) } }
然后替换你原来的调用逻辑:
self.dataAccessService.fetchRepliesByCommentId(completionHandler: { commentReplyArray in var localDataSource = [CommentItem]() // 用局部变量避免外部线程干扰 var localIndexPaths = [IndexPath]() self.processReplies(commentReplyArray, currentIndex: 0, baseInsertIndex: index, dataSource: &localDataSource, indexPaths: &localIndexPaths) { // 可选:处理完成后的额外操作 } }, commentId: commentItem.commentObject.id)
方案2:用DispatchSemaphore阻塞顺序执行(需注意队列)
通过信号量让循环等待当前异步请求完成后再继续,注意要放在后台队列避免阻塞主线程:
self.dataAccessService.fetchRepliesByCommentId(completionHandler: { commentReplyArray in var localDataSource = [CommentItem]() var localIndexPaths = [IndexPath]() let semaphore = DispatchSemaphore(value: 0) // 在后台队列执行循环,避免阻塞主线程 DispatchQueue.global().async { for (i, reply) in commentReplyArray.enumerated() { self.fetchDetailsAboutCommentReply(commentReplyObject: reply) { commentItem in commentItem.commentObject = reply let insertIndex = index + i + 1 // 回到主线程修改数据源(UI相关操作必须在主线程) DispatchQueue.main.sync { localDataSource.insert(commentItem, at: insertIndex) localIndexPaths.append(IndexPath(row: insertIndex, section: 0)) } // 信号量发信号,告诉循环可以继续了 semaphore.signal() } // 等待当前异步请求完成 semaphore.wait() } // 所有处理完成后回到主线程回调代理 DispatchQueue.main.async { self.delegate?.didLoadReplies(dataSource: localDataSource, ips: localIndexPaths) } } }, commentId: commentItem.commentObject.id)
方案3:用Async/Await重构(iOS 15+ 推荐)
如果你的项目支持iOS 15及以上,用苹果推荐的async/await可以让代码更简洁易读,彻底摆脱回调地狱:
首先把你的异步函数转成async版本:
// 封装fetchRepliesByCommentId为async函数 private func fetchRepliesByCommentId(commentId: String) async throws -> [CommentReply] { return try await withCheckedThrowingContinuation { continuation in self.dataAccessService.fetchRepliesByCommentId(completionHandler: { replies in continuation.resume(returning: replies) }, commentId: commentId) } } // 重构fetchDetailsAboutCommentReply为async版本 private func fetchDetailsAboutCommentReply(commentReplyObject: CommentReply) async -> CommentItem { let commentItem = CommentItem() // 异步获取用户信息 let userObject = await withCheckedContinuation { continuation in self.dataAccessService.fetchUserById(completionHandler: continuation.resume(returning:), uid: commentReplyObject.userId) } commentItem.userObject = userObject // 异步获取头像URL let contentURL = await withCheckedContinuation { continuation in self.dataAccessService.fetchDownloadURLOfProfileImage(organizerId: commentReplyObject.userId, completionHandler: continuation.resume(returning:)) } commentItem.userObject.contentURL = contentURL return commentItem }
然后调用逻辑就变得非常清晰:
Task { do { let commentReplyArray = try await self.fetchRepliesByCommentId(commentId: commentItem.commentObject.id) var localDataSource = [CommentItem]() var localIndexPaths = [IndexPath]() // 顺序遍历,await会等待当前请求完成再继续 for (i, reply) in commentReplyArray.enumerated() { let commentItem = await self.fetchDetailsAboutCommentReply(commentReplyObject: reply) commentItem.commentObject = reply let insertIndex = index + i + 1 localDataSource.insert(commentItem, at: insertIndex) localIndexPaths.append(IndexPath(row: insertIndex, section: 0)) } // 回调代理 self.delegate?.didLoadReplies(dataSource: localDataSource, ips: localIndexPaths) } catch { // 处理错误情况 print("加载回复失败:\(error.localizedDescription)") } }
注意事项
- 不管用哪种方案,修改
dataSource这类UI相关的数据源时,一定要确保在主线程执行,避免UI异常。 - 尽量用局部变量存储中间结果(比如上面的
localDataSource),避免多线程同时修改同一个全局变量导致的线程安全问题。
内容的提问来源于stack exchange,提问作者Nazar Medeiros
相关产品推荐
相关产品推荐

