图像和图形的最佳实践(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 Security简介、使用与最佳实践

《SpringSecurity简介、使用与最佳实践》SpringSecurity是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架,本文给大家介绍SpringSec... 目录一、如何理解 Spring Security?—— 核心思想二、如何在 Java 项目中使用?——

防止Linux rm命令误操作的多场景防护方案与实践

《防止Linuxrm命令误操作的多场景防护方案与实践》在Linux系统中,rm命令是删除文件和目录的高效工具,但一旦误操作,如执行rm-rf/或rm-rf/*,极易导致系统数据灾难,本文针对不同场景... 目录引言理解 rm 命令及误操作风险rm 命令基础常见误操作案例防护方案使用 rm编程 别名及安全删除

C++统计函数执行时间的最佳实践

《C++统计函数执行时间的最佳实践》在软件开发过程中,性能分析是优化程序的重要环节,了解函数的执行时间分布对于识别性能瓶颈至关重要,本文将分享一个C++函数执行时间统计工具,希望对大家有所帮助... 目录前言工具特性核心设计1. 数据结构设计2. 单例模式管理器3. RAII自动计时使用方法基本用法高级用法

PHP应用中处理限流和API节流的最佳实践

《PHP应用中处理限流和API节流的最佳实践》限流和API节流对于确保Web应用程序的可靠性、安全性和可扩展性至关重要,本文将详细介绍PHP应用中处理限流和API节流的最佳实践,下面就来和小编一起学习... 目录限流的重要性在 php 中实施限流的最佳实践使用集中式存储进行状态管理(如 Redis)采用滑动

ShardingProxy读写分离之原理、配置与实践过程

《ShardingProxy读写分离之原理、配置与实践过程》ShardingProxy是ApacheShardingSphere的数据库中间件,通过三层架构实现读写分离,解决高并发场景下数据库性能瓶... 目录一、ShardingProxy技术定位与读写分离核心价值1.1 技术定位1.2 读写分离核心价值二

深入浅出Spring中的@Autowired自动注入的工作原理及实践应用

《深入浅出Spring中的@Autowired自动注入的工作原理及实践应用》在Spring框架的学习旅程中,@Autowired无疑是一个高频出现却又让初学者头疼的注解,它看似简单,却蕴含着Sprin... 目录深入浅出Spring中的@Autowired:自动注入的奥秘什么是依赖注入?@Autowired

MySQL分库分表的实践示例

《MySQL分库分表的实践示例》MySQL分库分表适用于数据量大或并发压力高的场景,核心技术包括水平/垂直分片和分库,需应对分布式事务、跨库查询等挑战,通过中间件和解决方案实现,最佳实践为合理策略、备... 目录一、分库分表的触发条件1.1 数据量阈值1.2 并发压力二、分库分表的核心技术模块2.1 水平分

SpringBoot通过main方法启动web项目实践

《SpringBoot通过main方法启动web项目实践》SpringBoot通过SpringApplication.run()启动Web项目,自动推断应用类型,加载初始化器与监听器,配置Spring... 目录1. 启动入口:SpringApplication.run()2. SpringApplicat

Java整合Protocol Buffers实现高效数据序列化实践

《Java整合ProtocolBuffers实现高效数据序列化实践》ProtocolBuffers是Google开发的一种语言中立、平台中立、可扩展的结构化数据序列化机制,类似于XML但更小、更快... 目录一、Protocol Buffers简介1.1 什么是Protocol Buffers1.2 Pro

linux安装、更新、卸载anaconda实践

《linux安装、更新、卸载anaconda实践》Anaconda是基于conda的科学计算环境,集成1400+包及依赖,安装需下载脚本、接受协议、设置路径、配置环境变量,更新与卸载通过conda命令... 目录随意找一个目录下载安装脚本检查许可证协议,ENTER就可以安装完毕之后激活anaconda安装更