对图像进行下采样以获得更好的内存消耗和UICollectionView性能

任何可能出错的地方都会出错。
墨菲定律

在开发的每个步骤中,我们都会做出影响应用程序整体性能的架构决策。 我们都非常了解功耗和内存消耗对于移动应用程序极为重要。 我们也知道可用内存和应用程序的相对性能之间存在某种关联。 但是,在当今快速解决方案的世界中,缩短的期限和避免过早优化的精神使您容易错过重要的事情。 让我们看一下常见任务-图片库。 对于各种图像布局,它看起来可能会有所不同。 但是它们的共同点是-一批图像同时显示在屏幕上。

问题定义

假设您已决定从服务器获取下载的图像,并将其显示在UIImageView 。 这种方法完全没错。 此外,Apple建议在所有常见情况下使用UIImageUIImageView来显示图像。 仅当您进行了某些特定的图像处理时例外。

让我们回到画廊。 可能您已经使用Simulator和最新的iPhone版本在不同的图像集上测试了该应用程序。 现在准备进行质量检查阶段。 Beta测试人员和QA工程师选择您的应用程序,然后您会看到以下看起来很奇怪的崩溃报告:

您将开始使用特定的图像集测试您的应用,然后看到以下内容:

几乎每个致力于性能最佳实践的WWDC会话都表示,iOS应用程序应使用尽可能少的内存。 内存是iOS上最受限制的资源。 系统可能要求的可用内存比其释放的速度快。 正如文档所述,此WWDC会话iOS没有传统的磁盘交换,而是使用内存压缩器技术。

普通用户的设备上有多个应用程序。 许多应用程序可能仍在后台,并继续消耗一些内存。 系统本身正在消耗的部分内存。 在这一点上,您可能认为仍然应该留有足够的内存来平稳地运行应用程序。 无论如何,iOS足够聪明,可以卸载一个或两个烦人的内存使用者。 但实际上,系统设置了内存限制,每个应用程序都可以使用该内存限制。 由于超出限制,前台应用程序中正在运行的应用程序可能会被关闭。

那么,为什么图像会导致这种后果呢?

图像渲染流程

在iOS中显示图像的最常见方法是使用UIImageViewUIImageUIImage类负责管理图像数据,转换,应用适当的比例因子。 UIImageView —用于在应用程序界面中显示图像。

在WWDC上:Apple的图像和图形最佳实践工程师提供了一个非常简单直观的图表,说明了其实际工作方式。 基于此,当您使用UIImageUIImageView绘制图像时,实际上需要执行几个步骤:

1.将压缩的图像数据加载到内存。
2.将压缩的图像数据转换为渲染系统可以理解的格式。
3.渲染解码图像。

让我们在这里停下来。 我们需要了解什么是图像,我们拥有哪种图像类型和格式以及如何存储图像。

图片类型

首先,有两种主要的图像类型:栅格(位图)和矢量。 光栅图像表示为由每个像素的编码后的单个值填充的矩形网格。 矢量图像是根据2D点定义的,由线,多边形和其他形状连接。 与栅格不同,矢量格式存储用于绘制图像的指令。

光栅图像和矢量图像各有优缺点,通常用于不同目的。 向量通常用于将要应用于物理产品,徽标,技术图纸,文本,图标等图像的图像,其中包含尖锐的几何形状。 矢量图像的主要优点是分辨率独立性。 这意味着可扩展性而又不损失清晰度和质量。 矢量图像使用从一个点到另一点的数学计算来形成线条和形状,这就是为什么它对每种分辨率和缩放都产生相同结果的原因。

栅格图像由特定数量的像素组成。 缩放栅格图像时,它会变得模糊和锯齿。 但是,光栅图像在像照片这样的复杂场景中效果更好。 例如,照片编辑最好使用光栅图像。 发生这种情况是因为光栅图像使用了大量不同颜色的像素。 通过更改每个像素的颜色,可以达到不同的阴影和灰度。

图像压缩

下一步是压缩,这是一个广泛的主题。 因此,我们仅表示在当前情况下很重要的一些观点。 压缩的目的是用于存储和传输目的的图像数据的冗余。 两种类型的图像压缩用于图像编码:

–无损(可逆)压缩;
–有损(不可逆)压缩。

使用无损压缩,图像质量保持不变。 可以将文件解压缩到原始质量。 有损压缩将永久删除数据,并且此过程不可逆。 这意味着这种压缩图像无法以原始质量进行解压缩。

JPEG实际上是iOS开发的图像格式(例如PNG)中最流行的格式,而JPEG实际上是光栅图像。 SVG格式(在Android而非iOS上更流行)是矢量图像。 例如,PNG是无损压缩类型,JPEG是有损的。 尽管存储方法有所不同,矢量图像也可能很大。

根据文档,iOS本机支持以下图像格式:

.png,.tiff或.tif,.jpeg或.jpg,.gif,.bmp或.BMPf,.ico,.cur,.xbm。

实际上,所有这些图形文件格式都是光栅图像。 因此,让我们通过压缩光栅图像来限制自己。

将压缩的图像数据加载到内存

缓冲区是一种内存,用于容纳要存储的短时间数据以供处理。 作为示例,缓冲器用于处理音频数据。 首先,将数据块加载到缓冲区中,并且播放器能够从此缓冲区播放数据,同时保留继续加载并追加到现有数据块中的播放器。 当UIImage加载图像时,压缩的图像数据将加载到数据缓冲区,该缓冲区实际上不描述图像像素。

下一个概念是帧缓冲区。 帧缓冲区是渲染命令和图形管道的最终目标。 它包含有关要渲染的数据的信息。 渲染器与帧缓冲区一起使用。 在这一点上,您可能有一个问题,即来自数据缓冲区的压缩编码图像如何变成帧缓冲区中每个像素的适当信息,以使渲染机制可以理解和应用。

解码。 理论

在解码阶段,压缩的图像数据将被解压缩并解码为GPU可以理解的格式。 然后将解码后的数据放入图像缓冲区,其中包含将图像数据转换成每个像素的图像信息。 正如我们之前所指出的,光栅图像是像素的集合。 每个像素代表一种特定的颜色。 因此,将分配给图像缓冲器的数量的存储器与图像的尺寸有关。

由一个或多个颜色成分以及alpha的其他成分表示的像素颜色(基于颜色空间)。 例如,在RGB颜色模型中,有3个通道-红色,绿色,蓝色。 RGBA为4,alpha是表示透明度的附加通道。 阅读“关于色彩空间”一节以获取有关数字色彩理论的更多信息。

像素格式包含以下信息:

–每个分量的位数,即像素中每个单独颜色分量的位数。
–每像素位数,即源像素的位数。 该值必须至少是每个组件的位数乘以每个像素的组件数。
–每行字节数。 图像中每水平行的字节数。

Quartz 2D中RGBA色彩空间的32位像素格式,取自官方文档:

iOS上的默认颜色空间是标准RGB(sRGB),每个像素产生4个字节。 要计算图像缓冲区的大小,我们需要获取特定颜色空间中单个像素颜色信息的大小,然后乘以图像中像素的总数。 让我们考虑实际情况。 我拍了一张JPG图片,其分辨率为3024 x 4032,大小为3.1 MB。 在这种情况下,分配的内存量应为:

3024 * 4032 * 4 = 48771072字节= 46.51172 MB

在WWDC的iOS iOS深度学习中演示了相同的计算方法,但这就是理论。 现在,我们需要在真实设备上进行测试,以检查内存分配是否确认了以上针对iOS栅格图像格式中最流行的格式(PNG和JPG)的计算。

解码。 考试

初始数据:

–磁盘上具有3024 * 4032和文件大小14.2 MB的PNG图像
–磁盘上具有3024 * 4032和文件大小3.1 MB的JPG图像(请记住JPG是有损的压缩图像)

测试装置:
– iPhone XS(iOS 12.1.4)

测试工具:
分配工具
–内存图调试器
– vmmap(显示分配给指定进程的虚拟内存区域)

  vmmap —摘要ImagePerfomanceTest.memgraph 

PNG图像的内存消耗测试结果:

分配堆栈跟踪如下:

因此,磁盘上文件大小的14.2 MB PNG图像在虚拟内存中变为46.5 MB。 在设备和iOS模拟器上都可以再现相同的结果。

有了JPG图像,事情就变得更加复杂。 在iOS模拟器上,JPG的内存消耗与PNG相同(46.5 MB)。 但是在一个真实的设备上,我有这个:

JPG图像的分配堆栈跟踪:

如您所见,物理占用空间和内存分配堆栈跟踪不同。 具有相同图像分辨率的JPG需要较少的内存消耗。 IOSurface IOSurface预期的46.5 MB,而是1760 IOSurface

结果表明,图像分辨率非常重要,但是还有其他一些因素会影响内存占用。

下采样

让我们复杂一些,想象一下您正在为专业摄影师和设计师开发一个应用程序。 他们正在上传高分辨率图像。 您将加载具有6000 x 4000及更大尺寸的图像,而不是3024 x 4032的照片。 现在返回我们的画廊,并在屏幕上同时显示10–20个此类图像。 即使将图像以较小的边界放置在“ UIImageView”中,所有这些图像的图像缓冲区仍会保留在内存中,而代表该图像的UIImage将仍然存在。

显然,我们需要更改应用程序以改善用户体验并避免应用程序崩溃,而我们的第一步是降低采样率。 我们有几种选择:
1.出于预览目的(如图库中的图像缩略图),下载调整大小的图像并在用户确实需要时下载完整图像。 原因非常清楚-您不需要高分辨率的3000 x 4000照片就可以以100×100的边界显示它。 具有API可以使用的库存图像源通常提供此类功能。 图片网址如下所示: https://{PATH_TO_IMAGE}/{IMAGE_ID}/{size}.jpg

同样的方法也可以在您的自定义服务器上使用。 此外,这种方法还为您带来了更多好处:更快地下载图像,使用更少的流量。 对于移动互联网有限的用户,最后一个极为重要。

2.如果您没有下载下采样图像的机会,则需要自己进行。 调整图像大小的方法有很多种,但是请记住,您需要在不将整个图像加载到内存的情况下进行调整。 在这种情况下,图像渲染流程将如下所示(基于“图像和图形最佳实践”):

CGImageSource对象抽象了数据读取任务,减少了通过原始内存缓冲区管理数据的需求。 CGImageSource可以从URL,CFData对象或数据使用者加载数据。

下面列出的图像降采样源代码:

 私人功能下采样(imageAt imageURL:URL,to pointSize:CGSize,scale:CGFloat)-> UIImage { 
 让imageSourceOptions = [kCGImageSourceShouldCache:false]作为CFDictionary 
 让imageSource = CGImageSourceCreateWithURL(imageURL as CFURL,imageSourceOptions)! 

让maxDimentionInPixels = max(pointSize.width,pointSize.height)*比例

让downsampledOptions = [kCGImageSourceCreateThumbnailFromImageAlways:true,
kCGImageSourceShouldCacheImmediately:true,
kCGImageSourceCreateThumbnailWithTransform:true,
kCGImageSourceThumbnailMaxPixelSize:maxDimentionInPixels]作为CFDictionary
 让downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource,0,downsampledOptions)! 

返回UIImage(cgImage:downsampledImage)
}

代码是这样的:

  1. 创建一个字典,该字典指定图像源创建选项。 在当前版本中,使用了kCGImageSourceShouldCache 。 尽管有明显的时刻,但此值指示是否应以解码形式缓存图像。 设置为false时,CoreGraphics仅保存在文件数据中,而不会立即对其进行解码。
  2. 从传递的URL创建图像源对象。
  3. 计算kCGImageSourceThumbnailMaxPixelSize的最大像素(宽度或高度)。 该值基于所需的缩略图大小和屏幕比例因子进行计算。
  4. 创建用于创建缩略图的选项字典。 它包含下一个值:
  • kCGImageSourceCreateThumbnailFromImageAlways —指示是否应从完整图像创建缩略图,即使图像源文件中存在缩略图也是如此。 此键的Bevare指定为true,但是未设置kCGImageSourceThumbnailMaxPixelSize - CoreGraphics将创建一个具有完整图像大小的缩略图。
  • kCGImageSourceShouldCacheImmediately —关于此参数的文档不是很讲究。 但是在WWDC会议上:提到了“图像和图形最佳实践”,通过将此选项设置为true我们告诉CoreGraphics,缩略图创建正是为它创建解码图像缓冲区的关键时刻。
  • kCGImageSourceCreateThumbnailWithTransform —指示应根据整个图像的方向和像素长宽比旋转和缩放缩略图。
  • kCGImageSourceThumbnailMaxPixelSize —缩略图的最大宽度和高度(以像素为单位)。 如果未指定此键,则缩略图的宽度和高度不受限制,并且缩略图可能与图像本身一样大。

5.创建位于图像源中指定位置的图像的CGImage缩略图。 图像源可以包含一个以上的图像,缩略图,每个图像的属性以及图像文件。 在这种情况下,我们指定索引为0,因为我们知道只有一个图像。
6.将CGImage转换为UIImage

有关此技术的更多信息,请参见文档部分“创建和使用图像源”和WWDC会话:“图像和图形最佳实践”。

显着提高了内存消耗。