iOS 图片渲染的原理1
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS 图片渲染的原理1相关的知识,希望对你有一定的参考价值。
参考技术A 图片体积(size)指的是图片文件占用的存储空间的大小。数字图片存储的时候如果不压缩体积会比较大。比如,一张 1920x1080 的 24 位彩色 BMP 图片的体积约为 6MB。压缩可以减小体积。压缩分有损(lossy)(常见如 JPEG 格式)和无损(lossless)(常见如 PNG 格式)两种。
无损压缩不会丢失图片的任何信息,而仅仅是通过减少重复达到缩小体积的目的。有损图片压缩技术利用人眼的特性,使得可以将部分图片细节丢掉而人眼无法区分(或者说区别不明显)一般而言,对于同一张原始图片,有损压缩得越厉害,得到的压缩后的图片偏离原始图片就越大,质量越低。
图片体积和尺寸、质量的关系比较复杂,因为涉及到图片内容的特性复杂图片更难压缩、简单图像更容易压缩)、压缩方法等多种因素的影响。通常可以这么理解:尺寸越大、质量越高,则体积越大。
答:我们平常大部分会使用UIImage imageNamed这样的API加载了本地图片,而网络图片则使用了SDWebImage或者YYWebImage等框架来加载。所以没有去细究。
问题二: 使用imageNamed,系统何时去解码,有没有缓存,缓存的大小是多少,有没有性能问题,和imageWithContentsOfFile有什么区别
答: 一一来解答这个问题
首先先说imageNamed和imageWithContentsOfFile有什么区别,想必大部分小伙伴都很清楚,因为这也是面试老生常谈的东西。imageNamed加载本地图片会缓存图片,也就是加载一千张相同的本地图片,内存中也只会有一份,而imageWithContentsOfFile不会缓存,也就是重复加载相同图片,在内存中会有多份图片数据。
imageNamed加载图片会将图片源数据和解码后的数据加载入内存缓存中,只有收到内存警告的时候才会释放,有兴趣的小伙伴可以自行调试一下。
对于 ios 系统而言,绝大部分场景下哪类数据占内存最多呢?当然是图片!需要注意的是,图片所占内存的大小与图片的尺寸有关,而不是图片的文件大小。
例如:有一个 590KB 的图片,分辨率是 2048px * 1536px,它实际使用的内存不是 590KB,而是2048 * 1536 * 4 = 12 MB。。
当你缩小一幅图像的时候,会按照取平均值的办法把多个像素点变成一个像素点,这个过程称为 Downsampling。
1. 图像渲染管线 (Image Rendering Pipeline)
从 MVC 架构的角度来说,UIImage 代表了 Model,UIImageView 代表了 View. 那么渲染的过程我们可以这样很简单的表示:
Model 负责加载数据,View 负责展示数据。
但实际上,渲染的流程还有一个很重要的步骤:解码(Decode)。
为了了解Decode,首先我们需要了解Buffer这个概念。
2. 缓冲区 (Buffers)
Buffer在计算机科学中,通常被定义为一段连续的内存,作为某种元素的队列来使用。
下面让我们来了解几种不同类型的 Buffer。
Image Buffers代表了图片(Image)在内存中的表示。每个元素代表一个像素点的颜色,Buffer 大小与图像大小成正比.
The frame buffer 代表了一帧在内存中的表示。
Data Buffers代表了图片文件(Image file)在内存中的表示。这是图片的元数据,不同格式的图片文件有不同的编码格式。Data Buffers不直接描述像素点。 因此,Decode这一流程的引入,正是为了将Data Buffers转换为真正代表像素点的Image Buffer
3. 解码(Decoding)
将Data Buffers解码到Image Buffers是一个CPU密集型的操作。同时它的大小是和与原始图像大小成比例,和 View 的大小无关。
想象一下,如果一个浏览照片的应用展示多张照片时,没有经过任何处理,就直接读取图片,然后来展示。那 Decode 时,将会占用极大的内存和 CPU。而我们展示的图片的 View 的大小,其实是完全用不到这么大的原始图像的。
如何解决这种问题呢? 我们可以通过 Downsampling 来解决,也即是生成缩略图的方式。
通过Downsampling,我们成功地减低了内存的使用,但是解码同样会耗费大量的 CPU 资源。如果用户快速滑动界面,很有可能因为解码而造成卡顿。
UITableViewDataSourcePrefetching
解决办法:Prefetching+Background decoding
Prefetch 是 iOS10 之后加入到 TableView 和 CollectionView 的新技术。我们可以通过tableView(_:prefetchRowsAt:)这样的接口提前准备好数据。有兴趣的小伙伴可以搜一下相关知识。
至于Background decoding其实就是在子线程处理好解码的操作。
iOS 图像渲染原理
参考技术A下图所示为 iOS App 的图形渲染技术栈,App 使用 Core Graphics 、 Core Animation 、 Core Image 等框架来绘制可视化内容,这些软件框架相互之间也有着依赖关系。这些框架都需要通过 OpenGL 来调用 GPU 进行绘制,最终将内容显示到屏幕之上。
UIKit 是 iOS 开发者最常用的框架,可以通过设置 UIKit组件的布局以及相关属性来绘制界面。
事实上, UIKit自身并不具备在屏幕成像的能力,其主要负责对用户操作事件的响应(UIView继承自UIResponder),事件响应的传递大体是经过逐层的 视图树 遍历实现的。
Core Animation 源自于Layer Kit,动画只是Core Animation特性的冰山一角。
Core Animation是一个复合引擎,其职责是 尽可能快地组合屏幕上不同的可视内容,这些可视内容可被分解成独立的图层(即 CALayer),这些图层会被存储在一个叫做图层树的体系之中 。从本质上而言, CALayer 是用户所能在屏幕上看见的一切的基础。
Core Graphics 基于 Quartz 高级绘图引擎,主要用于运行时绘制图像。开发者可以使用此框架来处理基于路径的绘图,转换,颜色管理,离屏渲染,图案,渐变和阴影,图像数据管理,图像创建和图像遮罩以及 PDF 文档创建,显示和分析。
当开发者需要在 运行时创建图像 时,可以使用Core Graphics去绘制。与之相对的是 运行前创建图像 ,例如用 Photoshop 提前做好图片素材直接导入应用。相比之下,我们更需要Core Graphics去在运行时实时计算、绘制一系列图像帧来实现动画。
Core Image 与 Core Graphics 恰恰相反,Core Graphics用于在 运行时创建图像 ,而Core Image是用来处理 运行前创建的图像 的。Core Image框架拥有一系列现成的图像过滤器,能对已存在的图像进行高效的处理。
大部分情况下,Core Image会在 GPU 中完成工作,但如果 GPU 忙,会使用 CPU 进行处理。
OpenGL ES (OpenGL for Embedded Systems,简称 GLES),是 OpenGL 的子集。在前面的 图形渲染原理综述 一文中提到过 OpenGL 是一套第三方标准,函数的内部实现由对应的 GPU 厂商开发实现。
Metal 类似于 OpenGL ES ,也是一套第三方标准,具体实现由苹果实现。大多数开发者都没有直接使用过Metal,但其实所有开发者都在间接地使用Metal。Core Animation、Core Image、SceneKit、SpriteKit等等渲染框架都是构建于Metal之上的。
当在真机上调试 OpenGL 程序时,控制台会打印出启用Metal的日志。根据这一点可以猜测,Apple 已经实现了一套机制将 OpenGL 命令无缝桥接到Metal上,由Metal担任真正于硬件交互的工作。
在前面的Core Animation简介中提到CALayer事实上是用户所能在屏幕上看见的一切的基础。为什么 UIKit 中的视图能够呈现可视化内容?就是因为UIKit中的每一个 UI 视图控件其实内部都有一个关联的 CALayer ,即 backing layer 。
由于这种一一对应的关系,视图层级拥有 视图树 的树形结构,对应 CALayer 层级也拥有 图层树 的树形结构。
其中,视图的职责是 创建并管理 图层,以确保当子视图在层级关系中 添加或被移除 时, 其关联的图层在图层树中也有相同的操作 ,即保证视图树和图层树在结构上的一致性。
那么为什么 CALayer 可以呈现可视化内容呢?因为CALayer基本等同于一个 纹理 。纹理是 GPU 进行图像渲染的重要依据。
纹理本质上就是一张图片,因此CALayer也包含一个 contents 属性指向一块缓存区,称为 backing store ,可以存放位图(Bitmap)。iOS 中将该缓存区保存的图片称为 寄宿图 。
图形渲染流水线支持从顶点开始进行绘制(在流水线中,顶点会被处理生成纹理),也支持直接使用纹理(图片)进行渲染。相应地,在实际开发中,绘制界面也有两种方式:一种是 手动绘制 ;另一种是 使用图片
对此,iOS 中也有两种相应的实现方式:
Contents Image 是指通过 CALayer 的 contents 属性来配置图片。然而,contents属性的类型为 id 。在这种情况下,可以给 contents属性赋予任何值,app 仍可以编译通过。但是在实践中,如果content的值不是 CGImage ,得到的图层将是空白的。
既然如此,为什么要将contents的属性类型定义为id而非 CGImage?这是因为在 Mac OS 系统中,该属性对CGImage和NSImage类型的值都起作用,而在 iOS 系统中,该属性只对CGImage起作用。
本质上,contents属性指向的一块缓存区域,称为 backing store ,可以存放 bitmap 数据。
Custom Drawing 是指使用 Core Graphics 直接绘制寄宿图。实际开发中,一般通过继承 UIView 并实现 -drawRect: 方法来自定义绘制。
虽然 -drawRect: 是一个 UIView 方法,但事实上都是底层的 CALayer 完成了重绘工作并保存了产生的图片。下图所示为 -drawRect: 绘制定义寄宿图的基本原理。
通过前面的介绍,我们知道了 CALayer 的本质,那么它是如何调用 GPU 并显示可视化内容的呢?下面我们就需要介绍一下 Core Animation 流水线的工作原理。
事实上,app 本身并不负责渲染,渲染则是由一个独立的进程负责,即 Render Server 进程。
App 通过 IPC 将渲染任务及相关数据提交给 Render Server 。 Render Server 处理完数据后,再传递至 GPU。最后由 GPU 调用 iOS 的图像设备进行显示。
Core Animation 流水线的详细过程如下:
对上述步骤进行串联,它们执行所消耗的时间远远超过 16.67 ms,因此为了满足对屏幕的 60 FPS 刷新率的支持,需要将这些步骤进行分解,通过流水线的方式进行并行执行,如下图所示。
在 Core Animation 流水线中,app 调用 Render Server 前的最后一步 Commit Transaction 其实可以细分为 4 个步骤:
Layout 阶段主要进行视图构建,包括: LayoutSubviews 方法的重载, addSubview: 方法填充子视图等。
Display 阶段主要进行视图绘制,这里仅仅是设置最要成像的图元数据。重载视图的 drawRect: 方法可以自定义 UIView 的显示,其原理是在 drawRect: 方法内部绘制寄宿图,该过程使用 CPU 和内存。
Prepare 阶段属于附加步骤,一般处理图像的解码和转换等操作。
Commit 阶段主要将图层进行打包,并将它们发送至 Render Server 。该过程会递归执行,因为图层和视图都是以树形结构存在。
iOS 动画的渲染也是基于上述 Core Animation 流水线完成的。这里我们重点关注 app 与 Render Server 的执行流程。
日常开发中,如果不是特别复杂的动画,一般使用 UIView Animation 实现,iOS 将其处理过程分为如下三部阶段:
以上是关于iOS 图片渲染的原理1的主要内容,如果未能解决你的问题,请参考以下文章