Swift问题:DispatchQueue.main.async运行一段时间后停止更新UI
解决UICollectionView多选后指示器不显示的问题
我帮你分析下代码里的问题,然后给出针对性的修复方案:
核心问题分析
- 多余的主线程异步包裹:
collectionView(_:didSelectItemAt:)本身就是在主线程执行的,你把cell.showIndicator()包在DispatchQueue.main.async里,会把显示指示器的任务推到主线程队列的末尾。当快速选中多张图片时,大量的UI任务会把这个显示操作挤到后面,甚至还没执行就被后续的hideIndicator覆盖了。 - Cell复用导致的对象错误:你直接持有
didSelect里拿到的cell引用,当快速操作时,这个cell很可能已经被CollectionView复用到其他位置了,后续对它的UI操作根本不会作用在原来选中的那个单元格上。 - 无限制的后台任务并发:一次性启动8-12个后台图片加载任务,会占用大量系统资源,导致主线程UI响应卡顿,指示器的显示自然会被延迟或忽略。
修复后的代码示例
// 可以在类里先定义一个控制并发的操作队列(可选但推荐) let imageLoadingQueue = OperationQueue() override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) // 限制最大并发数,避免资源过载 imageLoadingQueue.maxConcurrentOperationCount = 4 } override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { // 1. 直接在主线程获取当前cell,确保是选中的那个 guard let cell = collectionView.cellForItem(at: indexPath) as? YourCollectionViewCell else { return } // 2. 立即显示指示器,无需额外异步包裹 cell.showIndicator() // 3. 用操作队列控制并发,替代直接用global队列 let loadOperation = BlockOperation { [weak self] in guard let self = self else { return } // 后台加载图片 let image = HelperFunctions().getAssetImage(asset: self.images[indexPath.item].asset, thumbnail: false) // 4. 主线程同步更新数据源,保证线程安全 DispatchQueue.main.sync { self.images[indexPath.item].image = image self.images[indexPath.item].page = self.selectedImages.count + 1 self.selectedImages.append(SelectedImage(image: image, index: indexPath.item)) } // 5. 回到主线程更新UI,重新获取cell避免复用问题 DispatchQueue.main.async { // 先确认cell还在屏幕上 if let updatedCell = collectionView.cellForItem(at: indexPath) as? YourCollectionViewCell { updatedCell.imageView.image = image updatedCell.hideIndicator() updatedCell.setSelected(page: self.selectedImages.count) } } } imageLoadingQueue.addOperation(loadOperation) }
关键修改点说明
- 移除多余的
DispatchQueue.main.async:直接在didSelect里调用cell.showIndicator(),确保指示器立即显示。 - 避免持有cell强引用:后台任务完成后,通过
collectionView.cellForItem(at:)重新获取当前屏幕上的cell,避免操作已被复用的旧cell。 - 控制后台任务并发数:用
OperationQueue限制同时加载的图片数量,减少系统资源占用,提升整体响应速度。 - 线程安全的数据源更新:把数据源修改放到主线程同步操作里,避免多线程读写冲突导致的数据异常。
进阶优化方案(推荐)
如果你的cell有自定义的配置方法,可以把UI更新逻辑交给cell自己处理,这样更符合MVC模式:
// 你的CollectionViewCell类里添加配置方法 func configure(with model: YourImageModel) { if let image = model.image { imageView.image = image hideIndicator() setSelected(page: model.page) } else { showIndicator() imageView.image = nil } }
然后在后台任务完成后,直接刷新对应cell即可:
// 后台任务完成后的主线程代码替换成: DispatchQueue.main.async { collectionView.reloadItems(at: [indexPath]) }
这样cell会根据数据源的状态自动更新UI,彻底避免cell复用带来的问题。
内容的提问来源于stack exchange,提问作者drante
相关产品推荐
相关产品推荐

