UIImage是一个很常见的组件,最常见的就是通过name或者路径读取。

两种创建方式的解释

public init?(named name: String)

This method checks the system caches for an image object with the specified name and returns the variant of that image that is best suited for the main screen. If a matching image object is not already in the cache, this method creates the image from an available asset catalog or loads it from disk. The system may purge cached image data at any time to free up memory. Purging occurs only for images that are in the cache but are not currently being used.

此方法会从系统缓存检查是否具有指定名称的 UIImage 对象。如果缓存中不存在匹配的图像对象,则此方法将从 assets 目录创建图像或从磁盘加载图像。系统可以随时清除缓存的图像数据以释放内存。仅对缓存中但当前未使用的图像进行清除。

public init?(contentsOfFile path: String)

This method loads the image data into memory and marks it as purgeable. If the data is purged and needs to be reloaded, the image object loads that data again from the specified path.

此方法将图像数据加载到内存中并将其标记为可清除。如果清除数据并需要重新加载,则图像对象将从指定路径再次加载该数据。

内存读取的区别

方法一:创建的 UIImage 的加载到内存中后,会一直存在内存中,及时持有 UIImage 的对象(如 UIImageView)释放了也不会释放。以及加载到内存中时就是解码后的图片。

方法二:没有对象(如 UIImageView)持有该 UIImage,会从内存中释放,下次使用时会从指定的 path重新加载。在内存中不是解码后的图片,需要在渲染前额外进行解码操作。

一旦图片文件被加载就必须要进行解码,解码过程是一个相当复杂的任务,需要消耗非常长的时间。解码后的图片将同样使用相当大的内存。

图片解码区别

用于加载的CPU时间相对于解码来说根据图片格式而不同。对于PNG图片来说,加载会比JPEG更长,因为文件可能更大,但是解码会相对较快,而且Xcode会把PNG图片进行解码优化之后引入工程。JPEG图片更小,加载更快,但是解码的步骤要消耗更长的时间,因为JPEG解码算法比基于zip的PNG算法更加复杂。

当加载图片的时候,iOS通常会延迟解码图片的时间,直到加载到内存之后。这就会在准备绘制图片的时候影响性能,因为需要在绘制之前进行解码(通常是消耗时间的问题所在)。

解码比较耗时,正常情况下,耗时操作我们可以通过创建多线程来解决,但是imageWithContentsOfFile有一个延迟解码的问题,只有使用到UIImage时,即将 image 赋值给imageView的时候,才会加载image到内存以及解码。而给imageView赋值image又需要在主线程执行,所以采用多线程不会对性能有所提升,因为解码的时候还是会卡顿。

例如下面这段代码,虽然是在其他线程读取的path,创建了image对象,但是由于延迟解码的问题,这样在给imageView赋值时才解码,依旧是在主线程操作并没有改善性能。

DispatchQueue.global().async {
    let coverImage = UIImage(contentsOfFile: path)
    DispatchQueue.main.async {
        imageView.image = coverImage
    }
}

imageNamed方法会在加载图片之后立刻进行解码,所以如果上面代码中的coverImage是通过imageNamed创建的话就可以提升了解码加载。问题在于imageNamed只对从应用资源束中的图片有效,所以对用户生成的图片内容或者是下载的图片就没法使用了。

所以如果放到一个列表tableviewCell场景,如果大量图片使用contentsOfFile创建,每次重新加载解析就会可能出现滚动卡顿现象。

优化

既然如此,那么我们可以将解码的耗时操作放到其他线程,解决延迟解码。

可通过Image IO创建 UIImage,将解码提前,然后再通过多线程去解决耗时问题,就比如上面的代码,改为

DispatchQueue.global().async {
    let imageURL = URL(fileURLWithPath: path) as CFURL
    let options = [
        kCGImageSourceCreateThumbnailFromImageAlways: true,
        kCGImageSourceCreateThumbnailWithTransform: true,
        kCGImageSourceShouldCacheImmediately: true,
        kCGImageSourceThumbnailMaxPixelSize: 500
    ] as CFDictionary
    
    if let source: CGImageSource = CGImageSourceCreateWithURL(imageURL, nil),
       let imageRef: CGImage = CGImageSourceCreateThumbnailAtIndex(source, 0, options) {
        self.mCoverImage = UIImage(cgImage: imageRef)
    }
    DispatchQueue.main.async {
        imageView.image = self.mCoverImage
    }
}

缓存

像上面通过Image IO生成了UIImage对象之后,频繁使用的话,可以存储起来,例如代码中的self.mCoverImage,下次访问直接判断self.mCoverImage是否存在直接返回,而不需要每次频繁的的创建和解码。

效果对比

经过使用Image IO将解码提前之后,FPS基本没变化,而直接使用imageWithContentsOfFile则在首次显示时,FPS出现较严重的卡顿,首次显示完毕之后,后续用的缓存的coverImage,开始稳定流畅

Image IOimageWithContentsOfFile
imageIO.gifcontent.gif

总结

并不是说imageWithContentsOfFile延迟解码不好,而是使用场景不同的原因,本意imageNamed用在频繁使用的小图片,imageWithContentsOfFile用在不频繁使用的图片,大图片,不包含在 Bundle 中的图片。不频繁使用大图的前提下,延迟解码节省了内存,但是如果像本文中放到列表,却大量使用imageWithContentsOfFile解码渲染,又必须显示,就可以采取移除掉这个延迟解码的操作了。

参考文章


☟☟可点击下方广告支持一下☟☟

最后修改:2022 年 08 月 14 日
请我喝杯可乐,请随意打赏: ☞已打赏列表