iOS 图片解码、缓存与列表复用

举报
yd_223002268 发表于 2026/09/23 09:09:49 2026/09/23
【摘要】 iOS 图片解码、缓存与列表复用图片文件在磁盘上的体积与显示时的内存占用并不相同。高分辨率图片完整解码后会占用大量内存,列表滚动时若同步解码或旧请求回写新单元格,还会产生卡顿和串图。图片管线需要统一处理目标尺寸、缓存、取消和主线程更新。 一、按显示尺寸解码缩略图只需要接近控件像素尺寸的数据。直接创建原始图片对象,再交给视图缩小,无法避免完整解码成本。可以让图片数据提供者根据目标尺寸生成缩略...

iOS 图片解码、缓存与列表复用

图片文件在磁盘上的体积与显示时的内存占用并不相同。高分辨率图片完整解码后会占用大量内存,列表滚动时若同步解码或旧请求回写新单元格,还会产生卡顿和串图。图片管线需要统一处理目标尺寸、缓存、取消和主线程更新。

一、按显示尺寸解码

缩略图只需要接近控件像素尺寸的数据。直接创建原始图片对象,再交给视图缩小,无法避免完整解码成本。

可以让图片数据提供者根据目标尺寸生成缩略图:

struct ImageRequest: Hashable {
    let key: String
    let targetPixelSize: CGSize
    let scaleMode: ScaleMode
}

protocol ImageDataProviding {
    func loadData(for key: String) async throws -> Data
}

具体采样实现使用项目已有图片组件,并遵守最低系统版本。业务页面只传递稳定资源键和目标尺寸。

二、区分点与像素

视图布局尺寸通常以点表示,解码目标要结合屏幕缩放因子换算像素:

func pixelSize(for viewSize: CGSize, scale: CGFloat) -> CGSize {
    CGSize(
        width: viewSize.width * scale,
        height: viewSize.height * scale
    )
}

使用远大于目标的尺寸会浪费内存,过小则导致模糊。列表布局变化后,缓存键也要反映真正影响结果的尺寸档位。

三、使用有容量边界的内存缓存

NSCache 可以在系统内存压力下自动清理对象,但仍需要合理成本:

final class ImageMemoryCache {
    private let cache = NSCache<NSString, UIImage>()

    init(totalCostLimit: Int) {
        cache.totalCostLimit = totalCostLimit
    }

    func image(for key: String) -> UIImage? {
        cache.object(forKey: key as NSString)
    }

    func insert(_ image: UIImage, for key: String, cost: Int) {
        cache.setObject(image, forKey: key as NSString, cost: cost)
    }
}

成本可以按解码像素估算。缓存不是数据权威来源,被清除后必须能重新加载。

四、合并相同在途请求

同一图片同时出现在多个位置时,可以共享加载任务。最后一个使用者取消后,再决定是否取消底层任务。

在途键应包含资源版本、尺寸和变换。只按原始标识合并,可能把头像尺寸结果错误用于大图。

五、解决单元格串图

单元格需要持有当前图片任务,并在复用时取消:

final class CoverCell: UICollectionViewCell {
    private var imageTask: Task<Void, Never>?
    private var representedKey: String?

    override func prepareForReuse() {
        super.prepareForReuse()
        imageTask?.cancel()
        imageTask = nil
        representedKey = nil
        coverView.image = placeholderImage
    }

    func configure(with model: CoverModel) {
        representedKey = model.imageKey
        imageTask = Task { [weak self] in
            guard let image = await imageLoader.image(for: model.imageKey) else {
                return
            }
            guard !Task.isCancelled else { return }
            await MainActor.run {
                guard self?.representedKey == model.imageKey else { return }
                self?.coverView.image = image
            }
        }
    }
}

取消减少无用工作,键比较防止已经完成的旧结果覆盖新内容。

六、把解码与变换移出主线程

数据读取、图片解码、圆角裁剪和模糊处理不应阻塞主线程。最终设置视图必须回到主执行域。

系统图形对象并非所有操作都适合任意线程,具体实现应使用项目验证过的图片管线,不在业务层随意并发调用。

七、磁盘缓存要有失效策略

磁盘缓存减少重复读取,但需要:

  • 容量和淘汰规则。
  • 资源版本参与缓存键。
  • 账户隔离和退出清理。
  • 敏感图片是否允许落盘。
  • 写入失败时不影响主业务流程。

用户头像更新后,应使用新版本键或明确删除旧缓存,不能只等待自然过期。

八、预取需要可取消

列表预取可以提前准备即将显示的图片,但快速滚动会产生大量无用任务。预取接口应支持取消,并与正常加载共享缓存和在途请求。

纯文本列表或很短列表没有必要启用复杂预取。

九、处理内存警告

收到内存压力信号后,图片缓存应按项目策略清理。当前屏幕正在显示的图片由视图持有,不会因为缓存清理立即消失。

不要在每次进入后台都无条件删除所有磁盘缓存,频繁重建可能增加耗电和通信成本。

十、测量真实场景

测试长列表快速滚动、图片详情反复进入、横竖屏切换和低内存场景。关注主线程解码、峰值内存、缓存命中和单元格串图。

瞬时峰值可能来自并发解码,持续增长则需要检查任务、缓存键和视图生命周期。

总结

iOS 图片性能的关键是按目标像素尺寸解码,用有边界的统一缓存复用结果,并让列表任务支持取消和身份校验。后台完成读取与变换,主线程只更新视图,再通过真实滚动测量内存峰值,才能兼顾清晰度与流畅度。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。