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

Swift iOS:DispatchGroup结合URLSession致APP其他页面卡顿排查

问题分析与解决方案

我来帮你排查下这个卡顿问题,核心问题集中在DispatchGroup逻辑错误、线程不安全的变量操作以及自定义Alert的主线程阻塞这几个点,下面一步步拆解并给出修复方案:

1. 致命逻辑错误:DispatchGroup的notify位置放错了

你现在把myGroup.notify写在了for循环内部,这意味着每循环一次就会注册一个通知回调,最终会多次触发sendDataToFirebaseDatabase——不仅逻辑混乱,还会导致主线程堆积大量不必要的任务,这是卡顿的核心诱因之一。

修复方式:把myGroup.notify移到for循环外面,确保所有图片上传任务完成后只触发一次数据库写入。

2. 线程安全隐患:count变量的非同步修改

count同时在URLSession后台回调、Firebase Storage后台回调中被修改,虽然你用了DispatchQueue.main.async包裹,但Firebase Storage的putData回调默认运行在后台队列,直接访问count会引发竞态条件,可能出现键名重复、leave调用次数不匹配等问题,进而导致主线程任务异常。

修复方式:用一个串行队列来管理count的读取和修改,确保线程安全。

3. 异常路径未处理:遗漏leave调用

在URLSession.dataTask的回调中,如果出现网络错误,你直接return但没有调用myGroup.leave(),这会导致DispatchGroup永远处于等待状态,后续的数据库写入和Alert无法触发,甚至会阻塞主线程。

修复方式:在所有异常分支中调用myGroup.leave(),保证DispatchGroup的计数平衡。

4. 自定义Alert的主线程优化

基于UIView实现的自定义Alert,如果初始化、布局计算恰好在tab切换的动画阶段执行(此时主线程本身有压力),就容易引发卡顿。建议确保Alert的显示是异步的,同时简化Alert的布局逻辑。


修正后的完整代码

var urls: [URL?] = [] // 假设是可选URL数组,避免强制解包
var picUUID = UUID().uuidString
var dict = [String: Any]()
let myGroup = DispatchGroup()
// 用串行队列保证count的线程安全
let countQueue = DispatchQueue(label: "com.yourapp.count.sync")
var count = 0

for url in urls {
    guard let validUrl = url else {
        myGroup.leave() // 空URL直接标记完成
        continue
    }
    myGroup.enter()
    URLSession.shared.dataTask(with: validUrl) { [weak self] data, response, error in
        guard let self = self else { 
            self?.myGroup.leave()
            return 
        }
        // 处理网络错误:必须调用leave
        guard let picData = data, error == nil else {
            self.myGroup.leave()
            return
        }
        // 线程安全地获取当前count并递增
        let currentCount: Int
        countQueue.sync {
            currentCount = self.count
            self.count += 1
        }
        self.sendDataToStorage("\(self.picUUID)_\(currentCount).jpg", picData: picData)
    }.resume()
}

// 把notify移到循环外面!确保所有任务完成后只触发一次
myGroup.notify(queue: .main) { [weak self] in
    guard let self = self else { return }
    self.sendDataToFirebaseDatabase()
    self.count = 0
}

func sendDataToStorage(_ picId: String, picData: Data) {
    dict.updateValue(picId, forKey:"picId_\(count)")
    let picRef = storageRoot.child("pics/\(picId)") // 明确子节点路径,避免错误
    picRef.putData(picData, metadata: nil) { [weak self] metadata, error in
        guard let self = self else { return }
        if let picUrl = metadata?.downloadURL()?.absoluteString {
            self.dict.updateValue(picUrl, forKey:"picUrl_\(count)")
        }
        // 无论上传成功失败,都标记任务完成
        self.myGroup.leave()
    }
}

func sendDataToFirebaseDatabase() {
    let ref = dbRoot.child("myRef")
    ref.updateChildValues(dict) { [weak self] error, ref in
        // 确保Alert在主线程异步显示,避免阻塞
        DispatchQueue.main.async {
            self?.displaySuccessAlert()
        }
    }
}

额外优化建议

  • 避免强制解包(比如原代码中的picData!),改用可选绑定处理空值,防止崩溃同时保证流程正常。
  • 自定义Alert实现中,把非UI初始化逻辑移到后台,只在主线程做最终显示;如果Alert有复杂动画,用UIViewPropertyAnimator优化性能。
  • 给Firebase Storage的putData指定后台队列,进一步减轻主线程压力:picRef.putData(picData, metadata: nil, queue: DispatchQueue.global()) { ... }

内容的提问来源于stack exchange,提问作者Lance Samaria

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 07:22:45