从S3存储桶加载图片时UITableView滚动卡顿,异步代码返回nil如何解决?
原始代码卡顿问题解决方案
卡顿原因
你当前的实现核心问题是Data(contentsOf:)属于同步阻塞方法,默认在主线程执行时会卡住所有UI响应(包括滚动、手势操作),直到图片下载完成才会恢复。同时代码没有做缓存处理,cell复用时会重复下载同一张图片,进一步加重性能损耗。
关于dequeueReusableCell的说法是正确的:如果不使用复用机制,每次新的cell进入可视区域都会重新创建完整的cell实例,滑动列表时会频繁触发对象创建、销毁操作,占用大量CPU和内存资源,也会引发卡顿。
优化方案
- 所有网络请求放到后台线程执行,避免阻塞主线程
- 增加二级缓存机制:内存缓存存储最近使用的图片,避免重复下载;磁盘缓存存储已下载过的图片,APP重启后也不用重新请求
- 处理cell复用逻辑:cell滑出可视区域时取消对应的未完成图片请求,同时每次cell复用时先设置占位图,避免出现旧图片或者空白
- 可选优化:图片下载完成后在后台线程做下采样,把图片分辨率压缩到和UIImageView的尺寸匹配,减少渲染时的CPU开销
如果不想自行实现全套逻辑,直接使用成熟的第三方图片加载库即可,上述能力这类库都已经封装完善。
异步代码返回nil的原因与修复
问题原因
URLSession.shared.dataTaskPublisher是异步操作,你当前的代码逻辑是发起请求后立刻返回image变量,此时网络请求还没有完成,receiveValue的回调还没触发,image变量还没有被赋值,自然每次都是nil。
除此之外你还有两个隐藏问题:
- 没有持有
sink返回的AnyCancellable实例,请求发起后task会立刻被释放,网络请求直接被取消,永远不会触发回调 - 没有把图片赋值操作切回主线程,直接在后台回调给UIImageView赋值会引发UI异常甚至崩溃
修复方案
不要用同步返回值的方式实现异步图片加载,改成回调/Combine绑定/async await的方式实现,示例代码如下:
回调方式示例
func loadImage(url: String, completion: @escaping (UIImage?) -> Void) -> URLSessionDataTask? { guard let validURL = URL(string: url) else { completion(nil) return nil } let task = URLSession.shared.dataTask(with: validURL) { data, _, error in guard let data = data, error == nil, let image = UIImage(data: data) else { DispatchQueue.main.async { completion(nil) } return } DispatchQueue.main.async { completion(image) } } task.resume() // task需要存到cell的成员变量里,cell复用时调用task.cancel()取消旧请求 return task } // cell里的调用代码 cell.cellImage.image = UIImage(named: "placeholder") // 先设置占位图 cell.currentLoadTask = loadImage(url: imageUrl) { [weak cell] image in cell?.cellImage.image = image }
Combine 正确写法示例
// 首先要在cell内部声明存储cancellable的变量 var cancellable = Set<AnyCancellable>() // cell复用时的加载代码 guard let validURL = URL(string: url) else { cell.cellImage.image = UIImage(named: "placeholder") return } // 先取消之前的未完成请求 cell.cancellable.removeAll() cell.cellImage.image = UIImage(named: "placeholder") URLSession.shared.dataTaskPublisher(for: validURL) .receive(on: DispatchQueue.main) // 结果切回主线程处理 .sink(receiveCompletion: { completion in if case .failure(let error) = completion { print("加载失败: \(error)") } }, receiveValue: { [weak cell] data, _ in cell?.cellImage.image = UIImage(data: data) }) .store(in: &cell.cancellable) // 持有cancellable,避免请求被提前释放
内容的提问来源于stack exchange,提问作者user15256253
相关产品推荐
相关产品推荐

