You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 12:17:37