从磁盘加载大量UIImage阻塞主线程的解决方案咨询
嘿,这个场景我太熟悉了——用20张本地图做单元格点击动画,直接主线程加载确实会卡得用户怀疑人生!给你两个靠谱的解决方案,还有一些优化细节:
方案1:后台预加载+内存缓存
核心思路是把图片加载逻辑丢到后台队列,避免阻塞主线程,同时用缓存重复利用已加载的图片,不用每次点击都重新读磁盘。
// 全局或类内定义内存缓存,自动处理内存不足时的清理 let hotDogImageCache = NSCache<NSString, UIImage>() // 后台加载所有动画帧的方法 func loadHotDogAnimationFrames(completion: @escaping ([UIImage]?) -> Void) { DispatchQueue.global(qos: .userInitiated).async { var animationFrames = [UIImage]() for frameIndex in 1...20 { let imageName = "hotdog_\(frameIndex)" // 先查缓存,命中直接用 if let cachedImage = hotDogImageCache.object(forKey: imageName as NSString) { animationFrames.append(cachedImage) continue } // 缓存没命中,从磁盘加载 guard let imagePath = Bundle.main.path(forResource: imageName, ofType: "png"), let image = UIImage(contentsOfFile: imagePath) else { continue } animationFrames.append(image) hotDogImageCache.setObject(image, forKey: imageName as NSString) } // 回到主线程回调结果 DispatchQueue.main.async { completion(animationFrames) } } } // 单元格点击事件处理 func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard let cell = collectionView.cellForItem(at: indexPath) as? HotDogCell else { return } // 先检查缓存是否已经存了所有帧 var cachedFrames = [UIImage]() var needReload = false for frameIndex in 1...20 { let key = "hotdog_\(frameIndex)" as NSString if let image = hotDogImageCache.object(forKey: key) { cachedFrames.append(image) } else { needReload = true break } } if !needReload { // 缓存命中,直接播放动画 cell.imageView.animationImages = cachedFrames cell.imageView.animationDuration = 1.2 // 根据需求调整动画时长 cell.imageView.startAnimating() } else { // 显示加载状态,比如给单元格加个活动指示器 cell.loadIndicator.startAnimating() loadHotDogAnimationFrames { [weak self] frames in guard let self = self, let validFrames = frames else { cell.loadIndicator.stopAnimating() return } cell.loadIndicator.stopAnimating() cell.imageView.animationImages = validFrames cell.imageView.animationDuration = 1.2 cell.imageView.startAnimating() } } }
方案2:逐帧懒加载(适合超长篇动画)
如果动画帧数更多(比如几十上百帧),预加载会占用大量内存,这时候可以用CADisplayLink逐帧加载显示,每次只处理当前需要的帧,内存压力小,也不会一次性卡顿。
class HotDogCell: UICollectionViewCell { @IBOutlet weak var imageView: UIImageView! private var displayLink: CADisplayLink? private var currentFrameIndex = 0 private let totalFrames = 20 private let frameCache = NSCache<NSString, UIImage>() // 启动动画 func startHotDogAnimation() { currentFrameIndex = 1 // 先显示第一帧 imageView.image = loadFrame(at: currentFrameIndex) // 创建屏幕刷新联动的定时器 displayLink = CADisplayLink(target: self, selector: #selector(updateAnimationFrame)) displayLink?.add(to: .current, forMode: .common) } // 停止动画 func stopHotDogAnimation() { displayLink?.invalidate() displayLink = nil } @objc private func updateAnimationFrame() { currentFrameIndex += 1 // 循环播放的话重置索引,否则停止动画 if currentFrameIndex > totalFrames { currentFrameIndex = 1 // 如果只播放一次,这里调用stopHotDogAnimation()即可 // stopHotDogAnimation() // return } // 后台加载当前帧 DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let self = self, let frameImage = self.loadFrame(at: self.currentFrameIndex) else { return } DispatchQueue.main.async { self.imageView.image = frameImage } } } // 加载单帧图片(带缓存) private func loadFrame(at index: Int) -> UIImage? { let key = "hotdog_\(index)" as NSString if let cachedImage = frameCache.object(forKey: key) { return cachedImage } guard let imagePath = Bundle.main.path(forResource: key as String, ofType: "png"), let image = UIImage(contentsOfFile: imagePath) else { return nil } frameCache.setObject(image, forKey: key) return image } // 单元格复用前清理状态 override func prepareForReuse() { super.prepareForReuse() stopHotDogAnimation() imageView.image = nil } } // 单元格点击时调用 func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard let cell = collectionView.cellForItem(at: indexPath) as? HotDogCell else { return } cell.startHotDogAnimation() }
额外优化小技巧
- 图片格式优化:把PNG转成HEIC或WebP格式,文件体积能缩小一半以上,加载速度会更快
- 提前预加载:在App启动后、用户滚动到单元格附近时,悄悄在后台加载图片,用户点击时就能直接播放
- 内存兜底:NSCache会在系统内存不足时自动释放缓存,不用手动管理,避免内存泄漏
内容的提问来源于stack exchange,提问作者slider
相关产品推荐
相关产品推荐

