ios图像和图形最佳实践(三)

2,367 阅读10分钟

没有前面进度的同学还是从(二)开始,否则会感觉比较突兀

ios图像和图形最佳实践(二)

..........

对于我们的app所附带的图片 苹果强烈建议我们使用图像素材来存储

这其中有很多原因

图像素材针对基于名称和基于特征的查找进行了优化

在素材目录中查找图片资源 会比搜索具有特定命名格式的磁盘上的文件要快得多(预置图像素材)

素材目录运行时在管理缓存大小方面也非常智能

还有一些与运行时性能无关的特性 是图像资源独有的 包括针对不同设备瘦身的功能 这意味着你的app只下载与其所运行的设备相关的图像资源 还有 Vector artwork 矢量图形功能

Vector artwork 是iOS11中引入的一项功能

你可以在素材编辑器中 选中“保留矢量数据 Preserve Vector Data” 复选框来启用它

其效果是如果你的图像在图像视图的渲染中 大于或小于图像的原始大小 它也不会变得模糊 这

种图像实际上是从矢量图形重新栅格化的 因此它具有很好的边缘清晰度

苹果在操作系统中的一个地方使用了这种技术

若你在 Accessibility设置中调整动态类型到一个非常大的尺寸 然后点击并按住标签栏中的项目 将会出现一个小HUD 显示当前你的手指所按住物体的放大视图

因此 如果你希望你的图片在这样的情境下看起来效果更好

那就选中图像素材管理器中的 "Preserve Vector Data" 复选框

它的工作方式 与前面提到的 管道非常类似

只是这里不是一个解码阶段,而是一个栅格化阶段 其负责获取矢量数据,并将其转化为可复制到帧缓存的位图数据

矢量图形管道 image.png

如果我们必须为app中所有矢量图形进行这项操作 我们会消耗更多的CPU资源 因此苹果在这里做了一个优化

如果你有一张选中了 “保留矢量数据”的图像 但你以正常的尺寸渲染它

实际上资源目录编译器已经生成了那个图片的预栅格化版本 并将其存储在资源目录中 因此并不需要做复杂的数学运算 来将矢量图形栅格化为位图

我们可以直接解码存储在资源目录中的图像 并将其直接渲染到帧缓存中

如果你计划以几种固定大小呈现图像

比如你有一个小图标和一个大图标 你不需要依赖 “Preserve Vector Data”复选框

只需创建你知道的两种尺寸两个图像资源 这将允许在编译期 而不是每次将图像复制到帧缓存时调用CPU对你的图片进行栅格化 从而达到优化的效果

不是每次将图像复制到帧缓存都进行计算

至此,我们已经看到了如何使用 UIImage 和 UIImageView 但你的app做的图形工作不止这些 有时app在运行时绘制内容

子类化UIView 并实现draw方法 这里的一个实现绘制了一个黄色的roundRect 绘制一些文字,并在其上绘制一个图像

image.png

image.png

出于若干原因 苹果并不推荐这种方法

image.png

我们将这个视图子类与UIImageView进行比较

你可能已经知道每个UIView实际上都是依赖 CoreAnimation运行时的CALayer实现的

image.png

对于我们的图像视图 图像视图要求图像创建解码图像缓存

image.png

然后将解码后的图像交给CALayer 用作其所在层的内容

image.png

image.png

对于我们重写draw得到的自定义视图 他们很相似 但略有不同 各个层负责创建图像缓存 以便保存我们的draw方法内容 然后我们的视图执行draw函数 并填充该图像缓存的内容

接着这些内容根据显示硬件的需要 被复制到帧缓存中

image.png

image.png

image.png

为了了解这将产生多大的开销 以及为什么我们不应该寻求实现这个UI的替代方法 我们在此使用的后备存储器 即连接到CALayer的图像缓存 其大小与我们正在显示的视图大小成正比

image.png

我们在iOS12中引入了一项新功能和优化

即后备存储器中元素的大小 实际上会动态增长 取决于你是否绘制任何有颜色的内容

该色彩内容 是在标准色彩范围之内或之外

因此如果你使用扩展的SRGB颜色 绘制广色域内容 则后备存储器实际上会比仅使用0到1范围内的颜色的后备存储器大

之前的iOS版本中,你可以通过设置CALayer的内容格式属性 来作为对 Core Animation的一个提示

即我知道我不需要在这个视图中支持广色域内容 或我知道我需要在这个视图中支持广色域内容

如果你这样做 你实际上将会禁用苹果在iOS12中引入的优化

因此 请检查layerWillDraw的实现 确保你不会以外关闭这项优化 该优化能使你的运行在iOS12上的代码收益无穷

但我们可以做到比仅仅提示我们是否需要一个支持广色域的后备存储器更好 我们实际上可以减少app所需的后备存储器总量

image.png

我们可以通过将这个较大的视图重构为较小的子视图来实现这一点 同时减少或消除重写draw函数的地方

这将帮助我们消除内存中图像数据的重复副本

并且这将允许我们利用UIView的优化属性 其不需实现后备存储器

image.png

因此正如前面提到的重写draw方法 将需要创建一个后备存储器 与CALayer一起使用

但是即使你不重写draw方法 UIView中的一些属性仍然可以工作

例如设置UIView的背景颜色 并不需要重建后备存储器 除非你使用的是图案颜色

因此苹果建议 不要在UIView中使用具有背景颜色属性的图案颜色 而应该创建一个UIImageView

将你的图像分配到该图像视图并使用 UIImageView中的函数 恰当的设置平铺参数

当我们想要剪切圆角矩形的角时 我们希望使用CALayer的cornerRadius属性

image.png

因为Core Animation能够渲染削角 而不需要额外的内存分配

如果我们改用更强大的maskView 或maskLayer属性 我们最终需要额外分配内存来存储该mask

如果你拥有更复杂透明区域的背景 并且不能通过cornerRadius属性进行设置 那么你应该考虑使用UIImageView 将这些信息存储在你的素材目录中 或在运行时渲染它 并将其作为图像提供给图像视图

而不应该使用 maskView或maskLayer

最后对于该Live Photo图标 UIImageView能够对单色图稿进行着色 而不需要额外的内存分配

image.png

你要做的第一件事是勾选 不是勾选复选框 而是在图片资源编辑器中将渲染模式属性设置为always template

或在UIImageView上调用withRenderingMode函数 来创建一个渲染模式为always template的UIImage

然后将该图像分配给图像视图 并将该图像视图的tintColor设置为你想要图像渲染的颜色

在UIImage将图像渲染到帧缓存过程中 它会在该复制操作中使用纯色 而不是坚持使用应用于纯色图像的单独副本

UIKit提供的视图中内置了另一项优化

image.png

UILabel可以在显示单色文本时比显示彩色文本或表情符号时 减少75%的内存使用

如果你想更详细了解此优化的工作原理 以及如何将其应用于UIView的自定义子类

可以参考“ios内存深潜”演讲 其详细介绍了这种名为A8的后备存储器格式

有时候 你想渲染存储在内存中图像缓存中的图像

UIKit为此提供的类是UIGraphicsImageRenderer

还有另一个更旧的函数UIGraphicsBeginImageContext 但请不要使用它 因为只有UIGraphicsImageRenderer 能够正确渲染广色域内容(屏外绘制)

你可以在app中使用 UIGraphicsImageRenderer 来渲染屏幕外的地方 然后使用UIImageView在屏幕上进行高效显示

与我们在CALayer后备存储器中引入的优化类似 我们也使 UIGraphicsImageRenderer 能够动态增长其图像缓存的大小 这取决于你在操作块中执行的操作

如果你在iOS12之前的操作系统上运行代码 你可以使用 UIGraphicsImageRenderFormat 中的 prefersExtendedRange 属性来告诉UIKit 你是否计划绘制广色域内容

但这里有一个中间地带

如果你主要将图像渲染到图形图像渲染器中 该图像可能使用超出SRGB色域的色彩空间值

但实际上并不需要更大的元素尺寸来存储这些信息

UIImage有一个可以用来获取一个预构建的 UIGraphicsImageRenderFormat 对象的 image renderer format属性

该对象用于在重新渲染图像时进行最优化存储

最后将谈一些 如何在你的app中集成

如果你需要使用Core Image 对你的图片实时进行大量的高级处理(高级图像处理) 那么请考虑使用 Core Image

Core Image是这样一个框架 它允许你创建处理图像的配方 并在CPU或GPU上进行处理

如果你从CIImage创建一个UIImage并将其交给UIImageView UIImageView将负责在GPU上执行该配方

这非常高效 并且它可以保持CPU空闲从而能够在你的app中执行其他工作

为了使用它像平常一样创建你的CIImage 然后调用UIImage CIImage初始化程序

image.png

iOS上还有其他用于处理和渲染图形内容的高级框架 包括 Metal Vision 和 Accelerate

这些框架中常见的数据类型之一 是 CVPixelBuffer

这是一种数据类型 用来表示在CPU或GPU上正在使用或尚未使用的缓存

在构建这些像素缓存的其中一个时 确保使用最好的初始化程序 即最接近你手头表述的那个

不要展开任何解码工作 这些工作已经由现有的UIImage 或 CGImage实现完成了

在CPU和GPU之间移动数据时要格外小心 这样你就不会在两者之间进行权衡工作 实际上你可以让它们并行执行

最后请关注一下Accelerate框架 “如何正确格式化待处理缓存”的 Accelerate和simd演讲

三篇文章总结一下关键点

  • 在表视图和集合视图中实现预取 以便可以事先完成一些工作并避免粘连

  • 确保你没有关闭UIKit提供的任何优化 这些优化可以减少与视图关联的后备存储器大小

  • 如果你将图像与app捆绑在一起 将其存储在资源目录中 不要将其存储在与你的app相关联的文件中

  • 如果你以不同大小渲染相同的图标 不要过分依赖“Preserve Vector Data”(保留矢量数据) 复选框