Swift中DispatchQueue.main.async加载图片导致搜索卡顿的解决办法
现有实现的核心卡顿原因
- 把同步网络请求、图片解码两类重度耗时操作全部绑定到主队列执行:
Data(contentsOf:)是阻塞式的网络IO,本身没有异步能力,即便外层包了DispatchQueue.main.async,这些耗时操作最终还是会占用主线程执行时间,阻塞UI渲染、滚动响应与搜索输入回调。 - 没有配套缓存机制:同一个股票logo每次单元格复用、滚动到可视区域时都会重复发起网络请求,快速滚动时短时间会堆叠大量无效请求,进一步抢占主线程资源。
- 缺少单元格复用校验:异步加载完成后没有核对当前单元格实际展示的内容,快速滚动时很容易出现图片错配,叠加搜索流程高频触发的
reloadData操作,卡顿感知会被放大。
无阻塞的图片加载落地逻辑
- 严格拆分任务执行队列:网络请求发起、原始图片数据解码这类耗时操作全部放到后台全局队列执行,只有更新UIImage、停止指示器这类必须操作UI的步骤,才切回主线程执行。
- 搭建简易缓存体系:用内存缓存存储已经解码完成的UIImage对象,用磁盘缓存存储下载完成的原始图片数据,同一URL的图片命中缓存时直接返回,不再发起网络请求。
- 增加复用一致性校验:异步图片加载完成后,先检查当前单元格需要展示的股票标识和之前发起请求时的标识是否一致,不一致就放弃更新UI,避免快速滚动时出现图片串位的问题。
- 对在途的相同请求做去重:同一个URL如果已经在加载中,就不要重复发起新的网络请求。
优化后的基础实现代码
DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let self = self else { return } let requestSymbol = stock.displaySymbol guard let imageURL = URL(string: "https://storage.googleapis.com/iex/api/logos/\(requestSymbol).png") else { return } // 命中缓存直接返回,不发起网络请求 if let cacheImage = ImageCache.shared.image(for: imageURL) { DispatchQueue.main.async { // 校验单元格当前展示内容匹配,避免串图 guard self.stock.displaySymbol == requestSymbol else { return } self.stockLogoImageView.image = cacheImage self.imageLoadingIndicator.stopAnimating() } return } // 后台队列执行同步网络请求,不阻塞主线程 guard let imageData = try? Data(contentsOf: imageURL), let loadedImage = UIImage(data: imageData) else { return } // 写入缓存 ImageCache.shared.store(image: loadedImage, for: imageURL) DispatchQueue.main.async { // 再次校验匹配关系 guard self.stock.displaySymbol == requestSymbol else { return } self.stockLogoImageView.image = loadedImage self.imageLoadingIndicator.stopAnimating() } }
搜索逻辑的配套优化
- 给搜索输入增加防抖逻辑:不要每输入一个字符就立刻执行全量数组过滤加
reloadData,延迟200~300ms、确认用户输入暂停后再执行刷新操作,减少无意义的表格重绘。 - 单元格复用时,提前取消对应cell未完成的图片加载请求,重置imageView为占位图状态,避免旧的加载任务在新的cell上触发UI更新。
内容的提问来源于stack exchange,提问作者MFAXXI
相关产品推荐
相关产品推荐

