Swift Concurrency中Data与URLSession在TaskGroup的表现差异排查
问题:Swift Concurrency下两种图片比例获取实现的差异及优化
问题背景
开发聊天APP时,通过API获取消息后,需要在刷新TableView前并发获取消息中图片的宽高比,采用Swift Concurrency实现。使用Data(contentsOf: URL)的同步实现(#1)时功能正常但有主线程阻塞警告;改用URLSession.shared.data(from: URL)的异步实现(#2)时,TableView为空,后续打印和刷新代码完全不执行,仅停留在任务组的遍历步骤。
差异原因分析
1. 任务组遍历逻辑错误
在fetchImageRatios()方法中,for try await ratio in group被嵌套在添加任务的内层循环中:
- 对于#1的同步实现:
Data(contentsOf:)会在子任务中同步阻塞并立即完成,遍历循环能快速拿到结果,继续外层循环添加剩余任务,整个流程能正常走完。 - 对于#2的异步实现:添加第一个任务后就进入遍历循环,该循环会阻塞当前执行流,直到所有已添加的任务完成,但此时外层循环还没来得及添加剩余的图片任务,导致任务组永远处于等待状态,后续的打印、UI刷新代码无法执行。
2. 值类型修改无效(隐藏问题)
Message是结构体(值类型),在for message in messageSection循环中,message是原数据的拷贝,修改message.ratio只会修改临时拷贝,不会影响messageSections中的原对象。#1中看似正常的显示可能是巧合,本质上该修改逻辑是无效的。
优化方案
1. 调整任务组执行逻辑
将任务添加和结果遍历分离,先批量添加所有图片比例获取任务,再统一处理结果。同时让任务返回包含Message匹配标识和比例的元组,确保能修改原数组中的对象:
private func fetchImageRatios() async throws { try await withThrowingTaskGroup(of: (String?, CGFloat?).self) { group in // 批量添加所有任务,传递图片URL作为匹配标识 for sectionIndex in messageSections.indices { for messageIndex in messageSections[sectionIndex].indices { let message = messageSections[sectionIndex][messageIndex] guard let imageURL = message.imageURL, let url = URL(string: imageURL) else { continue } group.addTask { let ratio = try await self.fetchRatio(from: url) return (imageURL, ratio) } } } // 遍历所有任务结果,修改原数组中的对应Message for try await (imageURL, ratio) in group { guard let imageURL = imageURL else { continue } for sectionIndex in messageSections.indices { if let messageIndex = messageSections[sectionIndex].firstIndex(where: { $0.imageURL == imageURL }) { messageSections[sectionIndex][messageIndex].ratio = ratio break } } } print("messageSections 1 - inside withThrowingTaskGroup closure") dump(messageSections) } print("messageSections 2 - outside withThrowingTaskGroup closure") dump(messageSections) }
2. 优化异步图片比例获取
保留URLSession的异步实现,添加超时和状态码校验,提升稳定性:
private func fetchRatio(from imageURL: URL) async throws -> CGFloat? { var request = URLRequest(url: imageURL) request.timeoutInterval = 10 // 设置10秒超时 let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { return nil } guard let image = UIImage(data: data) else { return nil } return image.size.height / image.size.width }
3. 简化UI刷新逻辑
使用MainActor.run替代DispatchQueue.main.async,符合Swift Concurrency的语法规范:
Task { [weak self] in do { try await self?.fetchImageRatios() await MainActor.run { self?.tableView.reloadData() } } catch { print("获取图片比例失败:\(error.localizedDescription)") } }
4. 容错处理优化
如果不需要因为单个图片获取失败中断整个流程,可以改用withTaskGroup,在任务内部捕获错误:
private func fetchImageRatios() async { await withTaskGroup(of: (String?, CGFloat?).self) { group in // 添加任务逻辑不变... for await result in group { // 处理结果,无需try捕获 guard let (imageURL, ratio) = result else { continue } // 修改原数组逻辑不变... } } }
内容的提问来源于stack exchange,提问作者hails
相关产品推荐
相关产品推荐

