图像和图形的最佳实践(WWDC 2018 session 219)

2024-04-26 02:48

本文主要是介绍图像和图形的最佳实践(WWDC 2018 session 219),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

该篇博客记录观看WWDC2018中Session219《Image And Graphics Best Practices》的内容及一些理解。

该Session主要讲述了关于有效使用图形内容的一些技术和策略。主要分三个方面:

  1. 从UIImage和UIImageView入手,讲述UIKit对于图形内容的处理。
  2. 讨论使用UIKit高效的处理自定义绘图。
  3. 简单讲述在应用中使用先进的CPU和GPU技术。

在整个讨论中,主要讨论图形处理对于内存CPU的影响,这两个因素可以影响到系统的反应速度以及电池的使用寿命。

UIImage和UIImageView

UIImage在UIKit中表示一个图形的内容,而UIImageView在UIKit中用来呈现一个视图。对应到MVC模式中,UIImage是一个加载图形内容的model,而UIImageView是显示渲染图形的视图。这两者之间的关系是一种连续的一次性的简单单向联系。如下图所示:
UIImage与UIImageView关系

但是在这之外还有一个隐藏的、影响程序性能的过程,叫做解码(Decode),如下图:
Decode

Decode

在了解Decode的过程中,我们首先要了解一个概念:缓冲区(Buffer)。

  1. 缓冲区是在内存中连续的区域。
  2. 通常被视为元素序列。
    缓冲区(Buffer)
1.图像缓冲区(Image Buffer)

图像缓冲区中每个元素表示的是图像中一个像素的颜色信息。所以,该Buffer在内存中的大小与图像大小成正比。

2.帧缓冲区(Frame Buffer)

帧缓冲区是保存应用实际呈现输出的缓冲区。

在应用更新视图层次结构时,UIKit会把应用程序的Window以及子视图渲染到帧缓冲区中。这个更新频率在iPhone上是60FPS,在iPad上是120FPS。
帧缓冲区(Frame Buffer)

3.数据缓冲区(Data Buffer)

数据缓冲区为保存一系列bytes数据的缓冲区。

在图像例子中,数据缓冲区就是保存从网络下载或者保存在磁盘中的图像的数据,这些数据并不直接描述每一个像素的信息。
数据缓冲区(Data Buffer)

加载过程

现在,我们了解了Buffer以及几种与图像相关的Buffer,接下来可以分析一下图片加载过程了。

  1. 加载准备:我们准备一个图像元数据,一个UIImage,以及一个加载在视图上的UIImageView,如下:
    加载准备
  2. 获取像素信息:为了将图像中每个像素信息填充到Frame Buffer中,我们需要得到图像像素信息。UIImage会为我们处理这一点:UIImage会创建一个与图片大小一致的Image Buffer用来存储解码(Decode)之后的图像像素信息。
    获取像素信息
  3. 显示在屏幕上:UIImageView读取UIImage创建的Image Buffer数据,交由UIKit显示。UIKit会对像素数据缩放到显示大小进行显示。
    显示在屏幕上

在加载过程中,UIKit会重复多次的要求UIImageView去进行渲染,这就造成了UIImage会多次的解码Data Buffer。

为了解决这个问题,UIImage会只进行一次Data Buffer的解码,在解码之后,会挂起解码后的Image Buffer。

所以,在这种情况下,应用对于每一个UIImage解码后,都会在内存中挂起一个Image Buffer。

由于Image Buffer与图像大小成正比,所以在加载大图片时,应用程序会在内存中挂起很多大的Image Buffer,这可能会造成操作系统介入,并最终杀死应用。

下采样(downsampling)

针对于我们上述提到的问题,我们可以采用叫做下采用(downsampling)的技术。

我们注意到,我们最终显示在屏幕上的视图往往比实际图片的尺寸要小,而通常情况下,Core Animation Framework会负责缩小图像。
缩放显示

现在要做的,本质上是把缩放图像操作捕获为一个叫缩略图的对象。由于减小了Image Buffer的大小,进而减小了在加载图片中的使用的内存总大小。

同时,我们在解码后,将图片像素信息交给UIImageView去渲染至屏幕后,可以丢弃掉对应的Data Buffer,进一步减少内存的使用。

这个过程如下图:
下采样过程

接下来我们放在代码中进行一下分析,整体代码如下:
下采样整体代码
接下来我们分析几个重要的点:

  1. 创建CGImageSourceRef
    创建CGImageSourceRef
    在创建CGImageSourceRef的时候,CGImageSourceCreate方法可以接受一个选项设置,我们可以提供kCGImageSourceShouldCachefalse。这个选项设置告诉Core Graphics Frameword我们只是需要一个CGImageSourceRef来存储对应路径的文件的信息,而不需要立刻去对文件中的图像进行解码。

  2. 计算需要渲染至屏幕的真实尺寸
    计算需要渲染至屏幕的真实尺寸

  3. 获取缩略图
    获取缩略图
    在获取缩略图的方法CGImageSourceCreateThumbnailAtIndex中,我们可以指定选项设置,其中需要我们关注的是kCGImageSourceShouldCacheImmediately(iOS7.0及以上版本),该设定告诉Core Graphics Frameword,在进行创建缩略图的时候,就应该立刻创建一个解码后的Image Buffer。

通过以上的改进方式,可以大幅度的缩减内存的使用。以下为一组测试数据:
测试环境:Xcode9.4,iPhone 6s Plus, 5184*3456的5.1MB大小的JPG格式图片。

  • 不使用改进方法,消耗内存为:49.3M
  • 使用改进方法,消耗内存为:9.4M

针对滚动视图的优化

如果我们需要一个滚动视图来展示一大组质量很大的图片时,我们也会遇到内存过高的问题,接下来我们以UICollectionView加载图片来做分析和优化。

使用下采样(downsampling)的方式优化

首先我们采用我们上一步优化图片的方式来为每一个cell中的图片进行优化,代码可能如下:
下采样优化滚动视图

这样看起来很有用,因为我们为每一个cell上的图片显示进行了优化,进而使得整体内存使用有一个明显的降低。但是,这样会引起另一个问题:如果此时滚动视图,CPU可能会很快的将所需展示的内容渲染到帧缓冲区,但是如果滚动过快,CPU还需参与Core Graphics对于新图像的解帧,这个操作是非常耗时的,这样就可能会造成在硬件读取帧缓冲区数据显示至屏幕时,帧缓冲区数据并没有准备好,进而导致用户视角中的卡顿。另外对于电池来说,在CPU不稳定时,可能会影响到电池使用寿命。

进一步优化

有两种技术可以帮助我们解决这个问题。

  1. 预取(prefetching):在某一时刻,我们不需要某个cell,但是在不久的将来会需要这个cell,所以可以把某些工作提前至这个时刻来进行。

  2. 后台执行(performing work in the background)

针对于UICollectionView,我们可以做以下处理:
预取和后台执行
我们为预取的cell进行在后台即全局异步队列(global
asynchronous queues)的图像解码,这是我们提到的两种优化技术。

但是,在使用全局异步队列的时候,可能会出现线程爆炸的问题:如果我们一次性处理多张图像,但是设备只有2个CPU时,此时GCD会创建新的线程来处理解码工作。创建新线程以及在不同线程之间切换是十分消耗时间和资源的。

解决线程爆炸问题

我们可以将解码操作异步的分派到串行队列中,如下图:
解决线程爆炸问题
这样做可能使得某些图像的解码过程延后,但是更多的是减少了在线程切换过程中浪费的时间和资源。

至此,我们完成了对于滚动视图加载多张图片的优化。

图片资源的优化

接下来分析一下我们对于图片资源的处理,在目前的程序中,可以展示的图像资源来源可能有一下几种:

  1. 存储在Image Assets中
  2. 存储在Application或者Bundle的包中
  3. 存储在沙盒的Document或者Cache文件中
  4. 从网络下载的数据中

对于程序中自带的图像资源,苹果官方推荐我们使用Image Assets来存储,以下是使用该方式的几个优点:

  1. Image Assets针对基于名称和特性的查找进行了优化,它比在磁盘上搜索文件要快。
  2. 在管理缓冲区方面也有优化。
  3. 允许对设备安装包进行优化,在下载App的时候会只下载能最高效果显示在对应设备的图像资源,从而减小安装包大小。
  4. 对Vector Artwork的支持。
Vector Artwork

Vector Artwork是iOS 11引入的新特性。我们可以在Image Assets中勾选Preserve Vector Data来启用它。如果启用了Vector Artwork的话,当我们将图像显示到一个比它原始尺寸大或者小的视图中时,这个图像不会变的模糊。因为显示的图像是从矢量图形中重新光栅化而来的,从而使得图像有清晰的边缘。

启用Vector Artwork后,图像的处理方式和之前图片的处理方式类似,只是将解码阶段变为了光栅化阶段,光栅化阶段将矢量数据转换为位图数据,进而供帧缓冲区读取。

Vector Artwork读取流程

如果我们把应用中所有的图像资源进行了Preserve Vector Data处理的话,会造成一些CPU的使用。
所以Xcode对这些情况做了一些处理:如果选中了Preserve Vector Data选项,但是在视图上展示为原始尺寸大小时,Image Assets实际上已经完成了原始尺寸的光栅化,并把相关数据保存在Image Assets中了,所以这种情况可以直接对存储的数据进行解码,而不进行光栅化。

另外一个建议是:如果计划展示的尺寸为固定的几个尺寸,那么不要依赖Preserve Vector Data,而是准备好对应尺寸的图像资源,从而加快CPU对图像资源的处理。

自定义绘制内容

我们有时需要在程序中进行一些自定义的绘制,例如绘制一个如下样式的视图:
自定义控件

我们可以继承UIView,然后在draw方法中进行相应的绘制:
自定义控件实现

但是我们并不推荐这么使用,我们首先对比UIImageView和draw的实现原理来进行分析:我们都知道UIView是基于CALayer来显示内容的;而对于UIImageView来说,UIImageView会将UIImage解码后的数据交给CALayer来作为内容显示;而对于draw来说,CALayer会创建一个与图像大小成正比的backing store来存储图像数据,然后将图像数据拷贝至backing store中,然后将backing store中的内容绘制到帧缓冲区中。

由此可见,通过重写draw方法可能会造成多余的内存消耗以及数据的拷贝,接下来了解一下backing store

Backing Store

在我们重写draw方法时,会触发创建Backing Store,此时的Backing Store的大小与视图的像素大小成正比。

同时,在iOS12中,Backing Store将会使用动态增长的方式来减少内存的使用。在以前的iOS版本中,我们可以设置CALayer的contentsFromat属性来指定Core Animation Framework在绘制时用到的颜色长度,这个设置会关闭iOS12中关于Backing Store的设置。

建议实现方式

回归主题,对于我们需要自定义绘制的视图,我们应该减少Backing Store的使用。通常的做法是将一个大视图构建为多个小视图来实现;同时,系统提供的经过优化的视图属性也不会造成多余内存的使用(例如UIView的backgroundColor属性,就不会创建Backing Store。此时需要注意,使用pattern color时是一个例外,可以使用UIImageView作为UIView的子视图来替代这一效果)。

将大视图构建为多个小视图

在Live视图中,我们可以构建为以下层级:
Live视图层级

1.圆角处理

在我们要进行圆角处理时,使用CALayer的cornerRadius属性,该属性不会引起多余的内存使用。而对于使用maskView和maskLayer来说,会使用到额外的内存空间。

同时对于圆角之外的透明区域为复杂的不规则形状且cornerRadius满足不了需求时,可以使用UIImageView配合Resizeable的UIImage来进行处理。

2.Icon的实现

Icon可以使用UIImageView来显示。

为了达到UIImage的复用,UIImageView支持对单色图像着色,同时可以直接渲染到帧缓冲区中。

这个设置需要对UIImage进行renderingMode的设置或者在Image Assets中进renderingMode设置。

最后,为UIImageView设置tintColor来完成最终颜色渲染。

3.UILabel的优化
  • UILabel对于显示单色字符串做了优化处理,可以节省75%大小的Backing Store的使用。

  • UILabel对于多彩字符串以及emoji进行了动态增长Backing Store的优化。

最后,如果想离屏绘制,就是用UIGraphicsImageRenderer,而不是使用旧版本的UIGraphicsBeginImageContext。因为UIGraphicsImageRenderer支持宽颜色内容展示;同时也支持根据绘制的动作来动态的增加Image Buffer的大小。在iOS12中,UIGraphicsImageRendererFormatprefersExtendedRange属性可以告知UIKit是否需要绘制宽颜色内容。同时,UIImage中提供了imageRendererFormat属性来配合UIGraphicsImageRenderFormat来使用。

简单讲述在应用中使用先进的CPU和GPU技术。

Advanced Image Effects

  1. 实时处理图像时考虑Core Image。
  2. 尽量使用GPU来处理图像,进而解放CPU。
  3. 使用CIImage创建UIImage,并交由UIImageView去展示,这个过程会默认由GPU去处理,减少对CPU的使用。

Advanced Image Processing

  1. 使用CVPixelBuffer将数据移动到Metal, Vision和Accelerate等框架。
  2. 使用最好的初始值设定项(不要重复执行已经完成的工作)。
  3. 防止在GPU和CPU直接来回切换。
  4. 确保交给Accelerate的buffers是正确的格式。

总结

  1. 异步的进行预处理。
  2. 使用UIImageView和UILabel来减少Backing Store的使用。
  3. 不要禁止自定义绘制时系统做的新优化。
  4. 使用Image Assets来存储图像。
  5. 如果要展示相同图片的不同尺寸,不要过多依赖Preserve Vector Data。

这篇关于图像和图形的最佳实践(WWDC 2018 session 219)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/936515

相关文章

Spring Boot循环依赖原理、解决方案与最佳实践(全解析)

《SpringBoot循环依赖原理、解决方案与最佳实践(全解析)》循环依赖指两个或多个Bean相互直接或间接引用,形成闭环依赖关系,:本文主要介绍SpringBoot循环依赖原理、解决方案与最... 目录一、循环依赖的本质与危害1.1 什么是循环依赖?1.2 核心危害二、Spring的三级缓存机制2.1 三

Python 中的 with open文件操作的最佳实践

《Python中的withopen文件操作的最佳实践》在Python中,withopen()提供了一个简洁而安全的方式来处理文件操作,它不仅能确保文件在操作完成后自动关闭,还能处理文件操作中的异... 目录什么是 with open()?为什么使用 with open()?使用 with open() 进行

OpenCV图像形态学的实现

《OpenCV图像形态学的实现》本文主要介绍了OpenCV图像形态学的实现,包括腐蚀、膨胀、开运算、闭运算、梯度运算、顶帽运算和黑帽运算,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起... 目录一、图像形态学简介二、腐蚀(Erosion)1. 原理2. OpenCV 实现三、膨胀China编程(

Spring Boot 配置文件之类型、加载顺序与最佳实践记录

《SpringBoot配置文件之类型、加载顺序与最佳实践记录》SpringBoot的配置文件是灵活且强大的工具,通过合理的配置管理,可以让应用开发和部署更加高效,无论是简单的属性配置,还是复杂... 目录Spring Boot 配置文件详解一、Spring Boot 配置文件类型1.1 applicatio

tomcat多实例部署的项目实践

《tomcat多实例部署的项目实践》Tomcat多实例是指在一台设备上运行多个Tomcat服务,这些Tomcat相互独立,本文主要介绍了tomcat多实例部署的项目实践,具有一定的参考价值,感兴趣的可... 目录1.创建项目目录,测试文China编程件2js.创建实例的安装目录3.准备实例的配置文件4.编辑实例的

Python 中的异步与同步深度解析(实践记录)

《Python中的异步与同步深度解析(实践记录)》在Python编程世界里,异步和同步的概念是理解程序执行流程和性能优化的关键,这篇文章将带你深入了解它们的差异,以及阻塞和非阻塞的特性,同时通过实际... 目录python中的异步与同步:深度解析与实践异步与同步的定义异步同步阻塞与非阻塞的概念阻塞非阻塞同步

Python Dash框架在数据可视化仪表板中的应用与实践记录

《PythonDash框架在数据可视化仪表板中的应用与实践记录》Python的PlotlyDash库提供了一种简便且强大的方式来构建和展示互动式数据仪表板,本篇文章将深入探讨如何使用Dash设计一... 目录python Dash框架在数据可视化仪表板中的应用与实践1. 什么是Plotly Dash?1.1

springboot集成Deepseek4j的项目实践

《springboot集成Deepseek4j的项目实践》本文主要介绍了springboot集成Deepseek4j的项目实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价... 目录Deepseek4j快速开始Maven 依js赖基础配置基础使用示例1. 流式返回示例2. 进阶

Android App安装列表获取方法(实践方案)

《AndroidApp安装列表获取方法(实践方案)》文章介绍了Android11及以上版本获取应用列表的方案调整,包括权限配置、白名单配置和action配置三种方式,并提供了相应的Java和Kotl... 目录前言实现方案         方案概述一、 androidManifest 三种配置方式

Spring Boot中定时任务Cron表达式的终极指南最佳实践记录

《SpringBoot中定时任务Cron表达式的终极指南最佳实践记录》本文详细介绍了SpringBoot中定时任务的实现方法,特别是Cron表达式的使用技巧和高级用法,从基础语法到复杂场景,从快速启... 目录一、Cron表达式基础1.1 Cron表达式结构1.2 核心语法规则二、Spring Boot中定