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

