如何解决UIImage加载大图,内存过大导致Carsh?
在本文中,与您分享我们面临的问题,高内存使用率的根本原因是什么,以及我们如何使用 Apple 工程师推荐的简单修复来减少应用程序的内存占用。
问题
如前所述,当我们开始在屏幕上加载高清图像时,我们的应用程序的内存使用量会激增。为了展示我们面临的问题,我创建了一个示例应用程序,当点击按钮时,它会将高清图像加载到图像视图中。
初始内存.png
@IBAction func loadImage(_ sender: Any) {
image.image = UIImage(named: "image.JPG")
}
截屏2021-06-30 上午11.55.17.png
加载图片后内存.png
请注意,我使用的是尺寸为 6240 × 4160px 的图像,其文件大小为 9MB。
如您所见,加载图像后内存使用量从 9MB 飙升至 155MB。
这完全没有道理!我们正在加载一个 9MB 的图像文件,内存占用怎么会这么高?所有这些内存使用来自哪里?😫
内存使用 ≠ 文件大小
我们用来解决这个问题的一种方法是缩小图像并根据图像视图的帧大小将其绘制在屏幕上,不幸的是,这似乎并没有解决我们的问题。
在没有任何关于如何解决问题的线索的情况下,我转向了网络。在谷歌搜索了几次之后,我得到了一个惊人的 WWDC 视频,它引入了一个我们都不知道的非常重要的概念:
内存使用与图像的尺寸有关,与文件大小无关。
为了在屏幕上显示图像,iOS 首先需要对图像进行解码和解压缩。通常,解码图像的 1 个像素将占用 4 个字节的内存
- 1 个字节用于红色,
- 1 个字节用于绿色,
- 1 个字节用于蓝色,
- 1 个字节用于 alpha 分量。
以我们尺寸为 6240 × 4160px 的示例图片为例:
(6240 × 4160) * 4字节 = 103833600 bytes
差不多 103.8336 MB
这确实与我们在内存报告中看到的想对接近一点了(由于图片场景格式问题,会有所偏差)
降采样来拯救
我们已经弄清楚内存使用的来源,但是我们如何将内存占用减少到可接受的水平呢?
正如我之前提到的,我们尝试缩小并重新绘制图像,但这似乎没有帮助。这是因为UIImage
调整大小和调整大小很昂贵。在调整大小的过程中,iOS 仍然会解码和解压缩原始图像,从而导致不必要的内存峰值。
使用 ImageIO 进行下采样
幸运的是,WWDC18 为我们面临的问题提供了很好的解决方案。我们可以使用该ImageIO
框架在图像显示在屏幕上之前调整其大小。这种ImageIO
方法的伟大之处在于,我们现在只需支付调整大小图像的成本就可以调整图像大小。我们称这种方法为“下采样”。
以下代码片段演示了如何对图像执行下采样:
func downsample(imageAt imageURL: URL,
to pointSize: CGSize,
scale: CGFloat = UIScreen.main.scale) -> UIImage? {
// Create an CGImageSource that represent an image
let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
guard let imageSource = CGImageSourceCreateWithURL(imageURL as CFURL, imageSourceOptions) else {
return nil
}
// Calculate the desired dimension
let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale
// Perform downsampling
let downsampleOptions = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels
] as CFDictionary
guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else {
return nil
}
// Return the downsampled image as UIImage
return UIImage(cgImage: downsampledImage)
}
让我们先来看看函数的参数:
- imageURL:图片网址。它可以是 Web URL 或本地图像路径。
- pointSize:下采样图像的所需大小。通常,这将是UIImageView的帧大小。
- scale:下采样比例因子。通常,这将是与屏幕相关的比例因子(我们通常将其称为@2x或@3x)。这就是为什么您可以看到其默认值已设置为UIScreen.main.scale.
选项标志
接下来,我想提请你注意选项标志3的功能被使用- kCGImageSourceShouldCache
,kCGImageSourceShouldCacheImmediately
和kCGImageSourceCreateThumbnailWithTransform
。
首先,让我们谈谈kCGImageSourceShouldCache
。当此标志设置为 false
时,我们让核心图形框架知道我们只需要创建对图像源的引用,并且不想CGImageSource
在创建对象时立即解码图像。
提示:在您无法访问图像源路径的情况下,您可以
CGImageSource
使用CGImageSourceCreateWithData()
初始化程序创建一个对象。
列表中的下一个是kCGImageSourceShouldCacheImmediately
。这个标志表示核心图形框架应该在我们开始下采样过程的那一刻解码图像。
因此,通过同时使用kCGImageSourceShouldCache
和kCGImageSourceShouldCacheImmediately
选项标志,我们可以完全控制何时要占用 CPU 进行图像解码。
最后是kCGImageSourceCreateThumbnailWithTransform
选项标志。将此标志设置为 true
非常重要,因为它让核心图形框架知道您希望下采样图像与原始图像具有相同的方向。
让我们看看结果
现在让我们尝试再次在屏幕上加载我们的示例图像,但这次启用了下采样。
let filePath = Bundle.main.url(forResource: "image", withExtension: "JPG")!
let downsampledLadyImage = downsample(imageAt: filePath, to: imageView.bounds.size)
imageView.image = downsampledLadyImage
最终内存.png
你已经看到了下采样的结果,它非常惊人。但这并不意味着您应该开始将高清图像捆绑到您的应用程序中并在需要时对它们进行下采样。
请记住,下采样是一个占用 CPU 能力的过程。因此,最好使用适当压缩后的图像源,而不是对 HD 图像进行下采样。
换句话说,只有当您需要显示尺寸远高于屏幕上所需尺寸的图像时,才应考虑使用下采样。
本文demo Github地址: https://github.com/stevendinggang/UIImageMemory