You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

从磁盘加载大量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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.21 03:41:32